diff --git a/AspNetCore.sln b/AspNetCore.sln index 8395a01cb39f..8320ce409030 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1762,6 +1762,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests", "src\Servers\Kestrel\Transport.NamedPipes\test\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj", "{97C7D2A4-87E5-4A4A-A170-D736427D5C21}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Generators", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.Generators.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10571,6 +10573,22 @@ Global {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x64.Build.0 = Release|Any CPU {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x86.ActiveCfg = Release|Any CPU {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x86.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|arm64.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|arm64.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x64.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x86.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|Any CPU.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|arm64.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|arm64.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x64.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x64.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x86.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11441,6 +11459,7 @@ Global {F057512B-55BF-4A8B-A027-A0505F8BA10C} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F} {10173568-A65E-44E5-8C6F-4AA49D0577A1} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} {97C7D2A4-87E5-4A4A-A170-D736427D5C21} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} + {4730F56D-24EF-4BB2-AA75-862E31205F3A} = {225AEDCF-7162-4A86-AC74-06B84660B379} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml index e2ec12ef3b32..2d1a36843ca4 100644 --- a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml @@ -1,7 +1,11 @@  - + PKV004 net7.0 + + PKV004 + net8.0 + \ No newline at end of file diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 9c1fc5cd35c8..c852d1016a13 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -77,6 +77,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant Private="false" ReferenceOutputAssembly="false" /> + + <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> + <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.Generators\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.Generators.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceAssemblyPath)" PackagePath="$(RefAssemblyPackagePath)" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceDocXml)" PackagePath="$(RefAssemblyPackagePath)" /> diff --git a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml index 056469f78b07..5beb41b8eec0 100644 --- a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml @@ -1,7 +1,11 @@  - + PKV0001 net7.0 + + PKV0001 + net8.0 + \ No newline at end of file diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj new file mode 100644 index 000000000000..171d742689ab --- /dev/null +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + true + false + true + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs new file mode 100644 index 000000000000..6aed91ce80b5 --- /dev/null +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +namespace Microsoft.AspNetCore.Http.Generators; + +[Generator] +public sealed class RequestDelegateGenerator : IIncrementalGenerator +{ + private static readonly string[] _knownMethods = + { + "MapGet", + "MapPost", + "MapPut", + "MapDelete", + "MapPatch", + "Map", + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var endpoints = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (node, _) => node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: IdentifierNameSyntax + { + Identifier: { ValueText: var method } + } + }, + ArgumentList: { Arguments: { Count: 2 } args } + } && _knownMethods.Contains(method), + transform: (context, token) => + { + var operation = context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation; + return StaticRouteHandlerModelParser.GetEndpointFromOperation(operation); + }) + .Where(endpoint => endpoint.Response.ResponseType == "string") + .WithTrackingName("EndpointModel"); + + var thunks = endpoints.Select((endpoint, _) => $$""" + [{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( + (del, builder) => + { + builder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + }, + (del, builder) => + { + var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; + EndpointFilterDelegate? filteredInvocation = null; + + if (builder.FilterFactories.Count > 0) + { + filteredInvocation = BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + } + {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} + }, + builder, + handler.Method); + } + + {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} + {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} + + return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + }), +"""); + + var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" +{{RequestDelegateGeneratorSources.GeneratedCodeAttribute}} +internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( + this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + {{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + } +"""); + + var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); + + context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) => + { + var (thunks, endpoints) = sources; + + var endpointsCode = new StringBuilder(); + var thunksCode = new StringBuilder(); + foreach (var endpoint in endpoints) + { + endpointsCode.AppendLine(endpoint); + } + foreach (var thunk in thunks) + { + thunksCode.AppendLine(thunk); + } + + var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource( + genericThunks: string.Empty, + thunks: thunksCode.ToString(), + endpoints: endpointsCode.ToString()); + context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code); + }); + } +} diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs new file mode 100644 index 000000000000..a92584bfbe68 --- /dev/null +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -0,0 +1,401 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Generators; + +internal static class RequestDelegateGeneratorSources +{ + private const string SourceHeader = """ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +using global::System; +using global::System.Collections; +using global::System.Collections.Generic; +using global::System.Diagnostics; +using global::System.Linq; +using global::System.Reflection; +using global::System.Threading.Tasks; +using global::System.IO; +using global::Microsoft.AspNetCore.Routing; +using global::Microsoft.AspNetCore.Routing.Patterns; +using global::Microsoft.AspNetCore.Builder; +using global::Microsoft.AspNetCore.Http; +using global::Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.FileProviders; +using global::Microsoft.Extensions.Primitives; +using MetadataPopulator = System.Action; +using RequestDelegateFactoryFunc = System.Func; +"""; + + public static string GeneratedCodeAttribute => $@"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]"; + + public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$""" +{{SourceHeader}} + +namespace Microsoft.AspNetCore.Builder +{ + {{GeneratedCodeAttribute}} + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } +} + +{{GeneratedCodeAttribute}} +internal static class GeneratedRouteBuilderExtensions +{ + private static readonly string[] GetVerb = new[] { HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; + + private static class GenericThunks + { + public static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{genericThunks}} + }; + } + + private static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{thunks}} + }; + + {{endpoints}} + + private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + string pattern, + System.Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populate, factory) = GenericThunks.map[(filePath, lineNumber)]; + return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); + } + + private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + string pattern, + System.Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populate, factory) = map[(filePath, lineNumber)]; + return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); + } + + private static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataSource(IEndpointRouteBuilder endpoints) + { + SourceGeneratedRouteEndpointDataSource? routeEndpointDataSource = null; + foreach (var dataSource in endpoints.DataSources) + { + if (dataSource is SourceGeneratedRouteEndpointDataSource foundDataSource) + { + routeEndpointDataSource = foundDataSource; + break; + } + } + if (routeEndpointDataSource is null) + { + routeEndpointDataSource = new SourceGeneratedRouteEndpointDataSource(endpoints.ServiceProvider); + endpoints.DataSources.Add(routeEndpointDataSource); + } + return routeEndpointDataSource; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, global::System.Reflection.MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static void PopulateMetadata(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider + { + T.PopulateMetadata(method, builder); + } + + private static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider + { + T.PopulateMetadata(parameter, builder); + } + + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return httpContext.Response.WriteAsJsonAsync(obj); + } + } + + {{GeneratedCodeAttribute}} + private sealed class SourceGeneratedRouteEndpointDataSource : EndpointDataSource + { + private readonly List _routeEntries = new(); + private readonly IServiceProvider _applicationServices; + + public SourceGeneratedRouteEndpointDataSource(IServiceProvider applicationServices) + { + _applicationServices = applicationServices; + } + + public RouteHandlerBuilder AddRouteHandler( + RoutePattern pattern, + Delegate routeHandler, + IEnumerable httpMethods, + bool isFallback, + MetadataPopulator metadataPopulator, + RequestDelegateFactoryFunc requestDelegateFactoryFunc) + { + var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + var routeAttributes = RouteAttributes.RouteHandler; + + if (isFallback) + { + routeAttributes |= RouteAttributes.Fallback; + } + _routeEntries.Add(new() + { + RoutePattern = pattern, + RouteHandler = routeHandler, + HttpMethods = httpMethods, + RouteAttributes = routeAttributes, + Conventions = conventions, + FinallyConventions = finallyConventions, + RequestDelegateFactory = requestDelegateFactoryFunc, + MetadataPopulator = metadataPopulator, + }); + return new RouteHandlerBuilder(new[] { new ConventionBuilder(conventions, finallyConventions) }); + } + + public override IReadOnlyList Endpoints + { + get + { + var endpoints = new RouteEndpoint[_routeEntries.Count]; + for (int i = 0; i < _routeEntries.Count; i++) + { + endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i]).Build(); + } + return endpoints; + } + } + + public override IReadOnlyList GetGroupedEndpoints(RouteGroupContext context) + { + var endpoints = new RouteEndpoint[_routeEntries.Count]; + for (int i = 0; i < _routeEntries.Count; i++) + { + endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions, context.FinallyConventions).Build(); + } + return endpoints; + } + + public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; + + private RouteEndpointBuilder CreateRouteEndpointBuilder( + RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList>? groupConventions = null, IReadOnlyList>? groupFinallyConventions = null) + { + var pattern = RoutePatternFactory.Combine(groupPrefix, entry.RoutePattern); + var handler = entry.RouteHandler; + var isRouteHandler = (entry.RouteAttributes & RouteAttributes.RouteHandler) == RouteAttributes.RouteHandler; + var isFallback = (entry.RouteAttributes & RouteAttributes.Fallback) == RouteAttributes.Fallback; + var order = isFallback ? int.MaxValue : 0; + var displayName = pattern.RawText ?? pattern.ToString(); + if (entry.HttpMethods is not null) + { + // Prepends the HTTP method to the DisplayName produced with pattern + method name + displayName = $"HTTP: {string.Join("", "", entry.HttpMethods)} {displayName}"; + } + if (isFallback) + { + displayName = $"Fallback {displayName}"; + } + // If we're not a route handler, we started with a fully realized (although unfiltered) RequestDelegate, so we can just redirect to that + // while running any conventions. We'll put the original back if it remains unfiltered right before building the endpoint. + RequestDelegate? factoryCreatedRequestDelegate = null; + // Let existing conventions capture and call into builder.RequestDelegate as long as they do so after it has been created. + RequestDelegate redirectRequestDelegate = context => + { + if (factoryCreatedRequestDelegate is null) + { + throw new InvalidOperationException("Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild"); + } + return factoryCreatedRequestDelegate(context); + }; + // Add MethodInfo and HttpMethodMetadata (if any) as first metadata items as they are intrinsic to the route much like + // the pattern or default display name. This gives visibility to conventions like WithOpenApi() to intrinsic route details + // (namely the MethodInfo) even when applied early as group conventions. + RouteEndpointBuilder builder = new(redirectRequestDelegate, pattern, order) + { + DisplayName = displayName, + ApplicationServices = _applicationServices, + }; + if (isRouteHandler) + { + builder.Metadata.Add(handler.Method); + } + if (entry.HttpMethods is not null) + { + builder.Metadata.Add(new HttpMethodMetadata(entry.HttpMethods)); + } + // Apply group conventions before entry-specific conventions added to the RouteHandlerBuilder. + if (groupConventions is not null) + { + foreach (var groupConvention in groupConventions) + { + groupConvention(builder); + } + } + // Any metadata inferred directly inferred by RDF or indirectly inferred via IEndpoint(Parameter)MetadataProviders are + // considered less specific than method-level attributes and conventions but more specific than group conventions + // so inferred metadata gets added in between these. If group conventions need to override inferred metadata, + // they can do so via IEndpointConventionBuilder.Finally like the do to override any other entry-specific metadata. + if (isRouteHandler) + { + entry.MetadataPopulator(entry.RouteHandler, builder); + } + // Add delegate attributes as metadata before entry-specific conventions but after group conventions. + var attributes = handler.Method.GetCustomAttributes(); + if (attributes is not null) + { + foreach (var attribute in attributes) + { + builder.Metadata.Add(attribute); + } + } + entry.Conventions.IsReadOnly = true; + foreach (var entrySpecificConvention in entry.Conventions) + { + entrySpecificConvention(builder); + } + // If no convention has modified builder.RequestDelegate, we can use the RequestDelegate returned by the RequestDelegateFactory directly. + var conventionOverriddenRequestDelegate = ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate) ? null : builder.RequestDelegate; + if (isRouteHandler || builder.FilterFactories.Count > 0) + { + factoryCreatedRequestDelegate = entry.RequestDelegateFactory(entry.RouteHandler, builder); + } + Debug.Assert(factoryCreatedRequestDelegate is not null); + // Use the overridden RequestDelegate if it exists. If the overridden RequestDelegate is merely wrapping the final RequestDelegate, + // it will still work because of the redirectRequestDelegate. + builder.RequestDelegate = conventionOverriddenRequestDelegate ?? factoryCreatedRequestDelegate; + entry.FinallyConventions.IsReadOnly = true; + foreach (var entryFinallyConvention in entry.FinallyConventions) + { + entryFinallyConvention(builder); + } + if (groupFinallyConventions is not null) + { + // Group conventions are ordered by the RouteGroupBuilder before + // being provided here. + foreach (var groupFinallyConvention in groupFinallyConventions) + { + groupFinallyConvention(builder); + } + } + return builder; + } + + private readonly struct RouteEntry + { + public MetadataPopulator MetadataPopulator { get; init; } + public RequestDelegateFactoryFunc RequestDelegateFactory { get; init; } + public RoutePattern RoutePattern { get; init; } + public Delegate RouteHandler { get; init; } + public IEnumerable HttpMethods { get; init; } + public RouteAttributes RouteAttributes { get; init; } + public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; } + public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions { get; init; } + } + + [Flags] + private enum RouteAttributes + { + // The endpoint was defined by a RequestDelegate, RequestDelegateFactory.Create() should be skipped unless there are endpoint filters. + None = 0, + // This was added as Delegate route handler, so RequestDelegateFactory.Create() should always be called. + RouteHandler = 1, + // This was added by MapFallback. + Fallback = 2, + } + + // This private class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. + private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List>, ICollection> + { + // We throw if someone tries to add conventions to the RouteEntry after endpoints have already been resolved meaning the conventions + // will not be observed given RouteEndpointDataSource is not meant to be dynamic and uses NullChangeToken.Singleton. + public bool IsReadOnly { get; set; } + void ICollection>.Add(Action convention) + { + if (IsReadOnly) + { + throw new InvalidOperationException("Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild"); + } + Add(convention); + } + } + + private sealed class ConventionBuilder : IEndpointConventionBuilder + { + private readonly ICollection> _conventions; + private readonly ICollection> _finallyConventions; + public ConventionBuilder(ICollection> conventions, ICollection> finallyConventions) + { + _conventions = conventions; + _finallyConventions = finallyConventions; + } + /// + /// Adds the specified convention to the builder. Conventions are used to customize instances. + /// + /// The convention to add to the builder. + public void Add(Action convention) + { + _conventions.Add(convention); + } + public void Finally(Action finalConvention) + { + _finallyConventions.Add(finalConvention); + } + } + } +} +"""; +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs new file mode 100644 index 000000000000..3d27d0e7b34c --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal static class StaticRouteHandlerModelEmitter +{ + /* + * TODO: Emit code that represents the signature of the delegate + * represented by the handler. When the handler does not return a value + * but consumes parameters the following will be emitted: + * + * ``` + * System.Action + * ``` + * + * Where `string` and `int` represent parameter types. For handlers + * that do return a value, `System.Func` will + * be emitted to indicate a `string`return type. + */ + public static string EmitHandlerDelegateType(Endpoint endpoint) + { + return $"System.Func<{endpoint.Response.ResponseType}>"; + } + + public static string EmitSourceKey(Endpoint endpoint) + { + return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; + } + + /* + * TODO: Emit invocation to the request handler. The structure + * involved here consists of a call to bind parameters, check + * their validity (optionality), invoke the underlying handler with + * the arguments bound from HTTP context, and write out the response. + */ + public static string EmitRequestHandler() + { + return """ +System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + var result = handler(); + return httpContext.Response.WriteAsync(result); + } +"""; + } + + /* + * TODO: Emit invocation to the `filteredInvocation` pipeline by constructing + * the `EndpointFilterInvocationContext` using the bound arguments for the handler. + * In the source generator context, the generic overloads for `EndpointFilterInvocationContext` + * can be used to reduce the boxing that happens at runtime when constructing + * the context object. + */ + public static string EmitFilteredRequestHandler() + { + return """ +async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await ExecuteObjectResult(result, httpContext); + } +"""; + } + + /* + * TODO: Emit code that will call the `handler` with + * the appropriate arguments processed via the parameter binding. + * + * ``` + * return System.Threading.Tasks.ValueTask.FromResult(handler(name, age)); + * ``` + * + * If the handler returns void, it will be invoked and an `EmptyHttpResult` + * will be returned to the user. + * + * ``` + * handler(name, age); + * return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + * ``` + */ + public static string EmitFilteredInvocation() + { + return "return System.Threading.Tasks.ValueTask.FromResult(handler());"; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs new file mode 100644 index 000000000000..b7a53d418fca --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal static class StaticRouteHandlerModelParser +{ + private const int RoutePatternArgumentOrdinal = 1; + private const int RouteHandlerArgumentOrdinal = 2; + + private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) + { + return new EndpointRoute(routePattern.ValueText); + } + + private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) + { + return new EndpointResponse(method.ReturnType.ToString(), "plain/text"); + } + + public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) + { + if (!TryGetRouteHandlerPattern(operation, out var routeToken)) + { + return null; + } + if (!TryGetRouteHandlerMethod(operation, out var method)) + { + return null; + } + var filePath = operation.Syntax.SyntaxTree.FilePath; + var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span); + + var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; + var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText; + + return new Endpoint(httpMethod, + GetEndpointRouteFromArgument(routeToken), + GetEndpointResponseFromMethod(method), + (filePath, span.EndLinePosition.Line + 1)); + } + + private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) + { + IArgumentOperation? argumentOperation = null; + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) + { + argumentOperation = argument; + } + } + if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || + routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) + { + token = default; + return false; + } + token = routePatternArgumentLiteralSyntax.Token; + return true; + } + + private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, out IMethodSymbol method) + { + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) + { + method = ResolveMethodFromOperation(argument); + return true; + } + } + method = null; + return false; + } + + private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch + { + IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), + IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), + IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), + IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => + ResolveMethodFromOperation(op), + IAnonymousFunctionOperation anon => anon.Symbol, + ILocalFunctionOperation local => local.Symbol, + IMethodReferenceOperation method => method.Method, + IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), + _ => null + }; + + private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) + { + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var syn = syntaxReference.GetSyntax(); + + if (syn is VariableDeclaratorSyntax + { + Initializer: + { + Value: var expr + } + }) + { + // Use the correct semantic model based on the syntax tree + var operation = semanticModel.GetOperation(expr); + + if (operation is not null) + { + return operation; + } + } + } + + return null; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs new file mode 100644 index 000000000000..fb3a60c63bbf --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal enum RequestParameterSource +{ + Query, + Route, + Header, + Form, + Service, + BodyOrService, +} + +internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue); +internal record EndpointRoute(string RoutePattern); +internal record EndpointResponse(string ResponseType, string ContentType); +internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location); diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 546bc7f16fb5..7d43a3aa61bb 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -4,6 +4,7 @@ $(DefaultNetCoreTargetFramework) $(Features.Replace('nullablePublicOnly', '') + true @@ -11,10 +12,16 @@ + + + + + + + - diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs new file mode 100644 index 000000000000..80ea89c6b40f --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Generators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.AspNetCore.Routing; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public class RequestDelegateGeneratorTestBase +{ + internal static (ImmutableArray, Compilation) RunGenerator(string sources) + { + var compilation = CreateCompilation(sources); + var generator = new RequestDelegateGenerator().AsSourceGenerator(); + + // Enable the source generator in tests + GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[] + { + generator + }, + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + + // Run the source generator + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, + out var _); + var diagnostics = updatedCompilation.GetDiagnostics(); + Assert.Empty(diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + var runResult = driver.GetRunResult(); + + return (runResult.Results, updatedCompilation); + } + + internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) + { + // We only invoke the generator once in our test scenarios + var firstGeneratorPass = results[0]; + if (firstGeneratorPass.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) + { + var staticEndpointStep = staticEndpointSteps.Single(); + var staticEndpointOutput = staticEndpointStep.Outputs.Single(); + var (staticEndpoint, _) = staticEndpointOutput; + var endpoint = Assert.IsType(staticEndpoint); + return endpoint; + } + return null; + } + + internal static Endpoint GetEndpointFromCompilation(Compilation compilation) + { + var assemblyName = compilation.AssemblyName!; + var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); + + var output = new MemoryStream(); + var pdb = new MemoryStream(); + + var emitOptions = new EmitOptions( + debugInformationFormat: DebugInformationFormat.PortablePdb, + pdbFilePath: symbolsName); + + var embeddedTexts = new List(); + + // Make sure we embed the sources in pdb for easy debugging + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var text = syntaxTree.GetText(); + var encoding = text.Encoding ?? Encoding.UTF8; + var buffer = encoding.GetBytes(text.ToString()); + var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true); + + var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot(); + var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: null, encoding: encoding, path: syntaxTree.FilePath); + + compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); + + embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText)); + } + + var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts); + + Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + Assert.True(result.Success); + + output.Position = 0; + pdb.Position = 0; + + var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb); + var handler = assembly.GetType("TestMapActions") + ?.GetMethod("MapTestEndpoints", BindingFlags.Public | BindingFlags.Static) + ?.CreateDelegate>(); + var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); + + Assert.NotNull(handler); + + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = handler(builder); + + var dataSource = Assert.Single(builder.DataSources); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); + Assert.NotNull(sourceKeyMetadata); + + return endpoint; + } + + private static Compilation CreateCompilation(string sources) + { + var source = $$""" +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +public static class TestMapActions +{ + public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder app) + { + {{sources}} + return app; + } +} +"""; + + var syntaxTrees = new[] + { + CSharpSyntaxTree.ParseText(source, path: $"TestMapActions.cs") + }; + + // Add in required metadata references + var resolver = new AppLocalResolver(); + var references = new List(); + var dependencyContext = DependencyContext.Load(typeof(RequestDelegateGeneratorTestBase).Assembly); + + Assert.NotNull(dependencyContext); + + foreach (var defaultCompileLibrary in dependencyContext.CompileLibraries) + { + foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(resolver)) + { + // Skip the source generator itself + if (resolveReferencePath.Equals(typeof(RequestDelegateGenerator).Assembly.Location, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + references.Add(MetadataReference.CreateFromFile(resolveReferencePath)); + } + } + + // Create a Roslyn compilation for the syntax tree. + var compilation = CSharpCompilation.Create(assemblyName: Guid.NewGuid().ToString(), + syntaxTrees: syntaxTrees, + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + return compilation; + } + + private sealed class AppLocalResolver : ICompilationAssemblyResolver + { + public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) + { + foreach (var assembly in library.Assemblies) + { + var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies ??= new(); + assemblies.Add(dll); + return true; + } + + dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies ??= new(); + assemblies.Add(dll); + return true; + } + } + + return false; + } + } + + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + { + public IServiceProvider ServiceProvider => this; + + public IServiceScope CreateScope() + { + return this; + } + + public void Dispose() { } + + public object GetService(Type serviceType) + { + return null; + } + } + + private class DefaultEndpointRouteBuilder : IEndpointRouteBuilder + { + public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) + { + ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder)); + DataSources = new List(); + } + + private IApplicationBuilder ApplicationBuilder { get; } + + public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); + + public ICollection DataSources { get; } + + public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs new file mode 100644 index 000000000000..606624f589db --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase +{ + [Theory] + [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] + [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] + public async Task MapAction_NoParam_StringReturn(string source, string expectedBody) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/hello", endpointModel.Route.RoutePattern); + + var httpContext = new DefaultHttpContext(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData("""app.MapGet("/hello", () => 2);""")] + [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] + public void MapGet_UnsupportedSignature_DoesNotEmit(string source) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + Assert.Null(endpointModel); + } +} diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 29d4f41c4e69..e0117d7cbeeb 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -20,6 +20,7 @@ "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", + "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.Generators.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", @@ -37,15 +38,15 @@ "src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj", - "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj", "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj", + "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj", + "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", + "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", - "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", - "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", @@ -61,8 +62,8 @@ "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", - "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", + "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj",