diff --git a/AspNetCore.sln b/AspNetCore.sln index 86cea0758faf..44be86231669 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1572,6 +1572,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BrowserTesting", "BrowserTe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.BrowserTesting", "src\Shared\BrowserTesting\src\Microsoft.AspNetCore.BrowserTesting.csproj", "{B739074E-6652-4F5B-B37E-775DC2245FEC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{722E5A66-D84A-4689-AA87-7197FF5D7070}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\Routing\samples\MapActionSample\MapActionSample.csproj", "{8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -7451,6 +7455,18 @@ Global {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x64.Build.0 = Release|Any CPU {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.ActiveCfg = Release|Any CPU {B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.Build.0 = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x64.Build.0 = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Debug|x86.Build.0 = Debug|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|Any CPU.Build.0 = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.ActiveCfg = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x64.Build.0 = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.ActiveCfg = Release|Any CPU + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -8227,6 +8243,8 @@ Global {22EA0993-8DFC-40C2-8481-8E85E21EFB56} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} {8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034} {B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D} + {722E5A66-D84A-4689-AA87-7197FF5D7070} = {54C42F57-5447-4C21-9812-4AF665567566} + {8F510BAA-FA6B-4648-8F98-28DF5C69DBB2} = {722E5A66-D84A-4689-AA87-7197FF5D7070} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Http/Http.Abstractions/src/IResult.cs b/src/Http/Http.Abstractions/src/IResult.cs new file mode 100644 index 000000000000..a6c20a6ebded --- /dev/null +++ b/src/Http/Http.Abstractions/src/IResult.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Defines a contract that represents the result of an HTTP endpoint. + /// + public interface IResult + { + /// + /// Write an HTTP response reflecting the result. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + Task ExecuteAsync(HttpContext httpContext); + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs new file mode 100644 index 000000000000..878ec45dcc8a --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using the request body. + /// + public interface IFromBodyMetadata + { + /// + /// Gets whether empty input should be rejected or treated as valid. + /// + bool AllowEmpty => false; + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs new file mode 100644 index 000000000000..054628d50069 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using form-data in the request body. + /// + public interface IFromFormMetadata + { + /// + /// The form field name. + /// + string? Name { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs new file mode 100644 index 000000000000..474daf9ed5bd --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using the request headers. + /// + public interface IFromHeaderMetadata + { + /// + /// The request header name. + /// + string? Name { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs new file mode 100644 index 000000000000..303f70c8ed3e --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using the request query string. + /// + public interface IFromQueryMetadata + { + /// + /// The name of the query string field. + /// + string? Name { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs new file mode 100644 index 000000000000..66b24e5284e9 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using route-data from the current request. + /// + public interface IFromRouteMetadata + { + /// + /// The name. + /// + string? Name { get; } + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs new file mode 100644 index 000000000000..92c1e2ad6758 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// + /// Interface marking attributes that specify a parameter should be bound using request services. + /// + public interface IFromServiceMetadata + { + } +} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 86c4ecf1bf30..28f8d0317d61 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -4,6 +4,19 @@ *REMOVED*Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object! value) -> bool *REMOVED*static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Type! middleware, params object![]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! *REMOVED*static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, params object![]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +Microsoft.AspNetCore.Http.IResult +Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata +Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool +Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata +Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata +Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata +Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata +Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string? +Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate? Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool diff --git a/src/Http/Http/src/QueryCollection.cs b/src/Http/Http/src/QueryCollection.cs index 75f1a47ea513..3753fd870bc7 100644 --- a/src/Http/Http/src/QueryCollection.cs +++ b/src/Http/Http/src/QueryCollection.cs @@ -24,7 +24,7 @@ public class QueryCollection : IQueryCollection private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; - private Dictionary? Store { get; set; } + private Dictionary? Store { get; } /// /// Initializes a new instance of . diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 7c36d355ae50..61dd3673f847 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -1,53 +1,54 @@ -{ +{ "solution": { "path": "..\\..\\AspNetCore.sln", - "projects" : [ + "projects": [ + "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", "src\\Http\\Authentication.Core\\test\\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", "src\\Http\\Headers\\test\\Microsoft.Net.Http.Headers.Tests.csproj", - "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", - "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.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\\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", "src\\Http\\Http.Features\\test\\Microsoft.AspNetCore.Http.Features.Tests.csproj", + "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj", + "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", + "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj", + "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", "src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj", "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj", - "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", - "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", - "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", - "src\\Http\\Http\\perf\\Microsoft.AspNetCore.Http.Performance.csproj", + "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", + "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj", "src\\Http\\Routing\\perf\\Microsoft.AspNetCore.Routing.Performance.csproj", + "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.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.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", - "src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj", - "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", - "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj", - "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj", "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj", - "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", - "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", - "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", - "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", - "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", - "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", - "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", - "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", - "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", - "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", + "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj", + "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj", "src\\Http\\WebUtilities\\perf\\Microsoft.AspNetCore.WebUtilities.Performance\\Microsoft.AspNetCore.WebUtilities.Performance.csproj", - "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", + "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", + "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", - "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", + "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", - "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj", - "src\\Testing\\src\\Microsoft.AspNetCore.Testing.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\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", + "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", + "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", + "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Http/Routing/samples/MapActionSample/MapActionSample.csproj b/src/Http/Routing/samples/MapActionSample/MapActionSample.csproj new file mode 100644 index 000000000000..6b59d1446b9b --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/MapActionSample.csproj @@ -0,0 +1,16 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + + + + + diff --git a/src/Http/Routing/samples/MapActionSample/Program.cs b/src/Http/Routing/samples/MapActionSample/Program.cs new file mode 100644 index 000000000000..0e33ff8e1f6f --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HttpApiSampleApp +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json b/src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json new file mode 100644 index 000000000000..af085af38a7e --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "HttpApiSampleApp": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Http/Routing/samples/MapActionSample/Startup.cs b/src/Http/Routing/samples/MapActionSample/Startup.cs new file mode 100644 index 000000000000..2efb7dd71e5b --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/Startup.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace HttpApiSampleApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + [HttpPost("/EchoTodo")] + JsonResult EchoTodo([FromBody] Todo todo) => new(todo); + + endpoints.MapAction((Func)EchoTodo); + + endpoints.MapPost("/EchoTodoProto", async httpContext => + { + var todo = await httpContext.Request.ReadFromJsonAsync(); + await httpContext.Response.WriteAsJsonAsync(todo); + }); + + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Hello World!"); + }); + }); + } + } +} diff --git a/src/Http/Routing/samples/MapActionSample/Todo.cs b/src/Http/Routing/samples/MapActionSample/Todo.cs new file mode 100644 index 000000000000..2bcc698c8e5a --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/Todo.cs @@ -0,0 +1,9 @@ +namespace HttpApiSampleApp +{ + public class Todo + { + public int Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } + } +} diff --git a/src/Http/Routing/samples/MapActionSample/appsettings.Development.json b/src/Http/Routing/samples/MapActionSample/appsettings.Development.json new file mode 100644 index 000000000000..8983e0fc1c5e --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Http/Routing/samples/MapActionSample/appsettings.json b/src/Http/Routing/samples/MapActionSample/appsettings.json new file mode 100644 index 000000000000..d9d9a9bff6fd --- /dev/null +++ b/src/Http/Routing/samples/MapActionSample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs b/src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs new file mode 100644 index 000000000000..4491b0305739 --- /dev/null +++ b/src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Builds conventions that will be used for customization of MapAction instances. + /// + public sealed class MapActionEndpointConventionBuilder : IEndpointConventionBuilder + { + private readonly List _endpointConventionBuilders; + + internal MapActionEndpointConventionBuilder(List endpointConventionBuilders) + { + _endpointConventionBuilders = endpointConventionBuilders; + } + + /// + /// 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) + { + foreach (var endpointConventionBuilder in _endpointConventionBuilders) + { + endpointConventionBuilder.Add(convention); + } + } + } +} diff --git a/src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000000..e132e673ea32 --- /dev/null +++ b/src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Provides extension methods for to define HTTP API endpoints. + /// + public static class MapActionEndpointRouteBuilderExtensions + { + /// + /// Adds a to the that matches the pattern specified via attributes. + /// + /// The to add the route to. + /// The delegate executed when the endpoint is matched. + /// An that can be used to further customize the endpoint. + public static MapActionEndpointConventionBuilder MapAction( + this IEndpointRouteBuilder endpoints, + Delegate action) + { + if (endpoints is null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate(action); + + var routeAttributes = action.Method.GetCustomAttributes().OfType(); + var conventionBuilders = new List(); + + const int defaultOrder = 0; + + foreach (var routeAttribute in routeAttributes) + { + if (routeAttribute.RoutePattern is not string pattern) + { + continue; + } + + var routeName = (routeAttribute as IRouteNameMetadata)?.RouteName; + var routeOrder = (routeAttribute as IRouteOrderMetadata)?.RouteOrder; + + var conventionBuilder = endpoints.Map(pattern, requestDelegate); + + conventionBuilder.Add(endpointBuilder => + { + foreach (var attribute in action.Method.GetCustomAttributes()) + { + endpointBuilder.Metadata.Add(attribute); + } + + endpointBuilder.DisplayName = routeName ?? pattern; + + ((RouteEndpointBuilder)endpointBuilder).Order = routeOrder ?? defaultOrder; + }); + + conventionBuilders.Add(conventionBuilder); + } + + if (conventionBuilders.Count == 0) + { + throw new InvalidOperationException("Action must have a pattern. Is it missing a Route attribute?"); + } + + return new MapActionEndpointConventionBuilder(conventionBuilders); + } + } +} diff --git a/src/Http/Routing/src/IRouteOrderMetadata.cs b/src/Http/Routing/src/IRouteOrderMetadata.cs new file mode 100644 index 000000000000..4326b24e5625 --- /dev/null +++ b/src/Http/Routing/src/IRouteOrderMetadata.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Interface for attributes which can supply a route order for attribute routing. + /// + public interface IRouteOrderMetadata + { + /// + /// Gets the route order. The order determines the order of route execution. Routes with a lower + /// order value are tried first. When a route doesn't specify a value, it gets a default value of 0. + /// A null value for the Order property means that the user didn't specify an explicit order for the + /// route. + /// + int? RouteOrder { get; } + } +} diff --git a/src/Http/Routing/src/IRoutePatternMetadata.cs b/src/Http/Routing/src/IRoutePatternMetadata.cs new file mode 100644 index 000000000000..615a67abfbeb --- /dev/null +++ b/src/Http/Routing/src/IRoutePatternMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Interface for attributes which can supply a route pattern for attribute routing. + /// + public interface IRoutePatternMetadata + { + /// + /// The route pattern. May be . + /// + string? RoutePattern { get; } + } +} diff --git a/src/Http/Routing/src/Internal/MapActionExpressionTreeBuilder.cs b/src/Http/Routing/src/Internal/MapActionExpressionTreeBuilder.cs new file mode 100644 index 000000000000..10be2443b5e7 --- /dev/null +++ b/src/Http/Routing/src/Internal/MapActionExpressionTreeBuilder.cs @@ -0,0 +1,463 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + internal static class MapActionExpressionTreeBuilder + { + private static readonly MethodInfo ChangeTypeMethodInfo = GetMethodInfo>((value, type) => Convert.ChangeType(value, type, CultureInfo.InvariantCulture)); + private static readonly MethodInfo ExecuteTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteTaskResultOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueResultTaskOfTMethodInfo = typeof(MapActionExpressionTreeBuilder).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetRequiredServiceMethodInfo = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; + private static readonly MethodInfo ResultWriteResponseAsync = typeof(IResult).GetMethod(nameof(IResult.ExecuteAsync), BindingFlags.Public | BindingFlags.Instance)!; + private static readonly MethodInfo StringResultWriteResponseAsync = GetMethodInfo>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default)); + private static readonly MethodInfo JsonResultWriteResponseAsync = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); + private static readonly MemberInfo CompletedTaskMemberInfo = GetMemberInfo>(() => Task.CompletedTask); + + private static readonly ParameterExpression TargetArg = Expression.Parameter(typeof(object), "target"); + private static readonly ParameterExpression HttpContextParameter = Expression.Parameter(typeof(HttpContext), "httpContext"); + private static readonly ParameterExpression DeserializedBodyArg = Expression.Parameter(typeof(object), "bodyValue"); + + private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.RequestServices)); + private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Request)); + private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextParameter, nameof(HttpContext.Response)); + + public static RequestDelegate BuildRequestDelegate(Delegate action) + { + // Non void return type + + // Task Invoke(HttpContext httpContext) + // { + // // Action parameters are bound from the request, services, etc... based on attribute and type information. + // return ExecuteTask(action(...), httpContext); + // } + + // void return type + + // Task Invoke(HttpContext httpContext) + // { + // action(...); + // return default; + // } + + var method = action.Method; + + var consumeBodyDirectly = false; + var consumeBodyAsForm = false; + Type? bodyType = null; + var allowEmptyBody = false; + + // This argument represents the deserialized body returned from IHttpRequestReader + // when the method has a FromBody attribute declared + + var args = new List(); + + foreach (var parameter in method.GetParameters()) + { + Expression paramterExpression = Expression.Default(parameter.ParameterType); + + if (parameter.GetCustomAttributes().OfType().FirstOrDefault() is { } routeAttribute) + { + var routeValuesProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.RouteValues)); + paramterExpression = BindParamenter(routeValuesProperty, parameter, routeAttribute.Name); + } + else if (parameter.GetCustomAttributes().OfType().FirstOrDefault() is { } queryAttribute) + { + var queryProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Query)); + paramterExpression = BindParamenter(queryProperty, parameter, queryAttribute.Name); + } + else if (parameter.GetCustomAttributes().OfType().FirstOrDefault() is { } headerAttribute) + { + var headersProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Headers)); + paramterExpression = BindParamenter(headersProperty, parameter, headerAttribute.Name); + } + else if (parameter.GetCustomAttributes().OfType().FirstOrDefault() is { } bodyAttribute) + { + if (consumeBodyDirectly) + { + throw new InvalidOperationException("Action cannot have more than one FromBody attribute."); + } + + if (consumeBodyAsForm) + { + ThrowCannotReadBodyDirectlyAndAsForm(); + } + + consumeBodyDirectly = true; + allowEmptyBody = bodyAttribute.AllowEmpty; + bodyType = parameter.ParameterType; + paramterExpression = Expression.Convert(DeserializedBodyArg, bodyType); + } + else if (parameter.GetCustomAttributes().OfType().FirstOrDefault() is { } formAttribute) + { + if (consumeBodyDirectly) + { + ThrowCannotReadBodyDirectlyAndAsForm(); + } + + consumeBodyAsForm = true; + + var formProperty = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); + paramterExpression = BindParamenter(formProperty, parameter, parameter.Name); + } + else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) + { + paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + } + else + { + if (parameter.ParameterType == typeof(IFormCollection)) + { + if (consumeBodyDirectly) + { + ThrowCannotReadBodyDirectlyAndAsForm(); + } + + consumeBodyAsForm = true; + + paramterExpression = Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form)); + } + else if (parameter.ParameterType == typeof(HttpContext)) + { + paramterExpression = HttpContextParameter; + } + } + + args.Add(paramterExpression); + } + + Expression? body = null; + + MethodCallExpression methodCall; + + if (action.Target is null) + { + methodCall = Expression.Call(method, args); + } + else + { + var castedTarget = Expression.Convert(TargetArg, action.Target.GetType()); + methodCall = Expression.Call(castedTarget, method, args); + } + + // Exact request delegate match + if (method.ReturnType == typeof(void)) + { + var bodyExpressions = new List + { + methodCall, + Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo) + }; + + body = Expression.Block(bodyExpressions); + } + else if (AwaitableInfo.IsTypeAwaitable(method.ReturnType, out var info)) + { + if (method.ReturnType == typeof(Task)) + { + body = methodCall; + } + else if (method.ReturnType.IsGenericType && + method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + var typeArg = method.ReturnType.GetGenericArguments()[0]; + + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + body = Expression.Call( + ExecuteTaskResultOfTMethodInfo.MakeGenericMethod(typeArg), + methodCall, + TargetArg, + HttpContextParameter); + } + else + { + // ExecuteTask(action(..), httpContext); + body = Expression.Call( + ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg), + methodCall, + TargetArg, + HttpContextParameter); + } + } + else if (method.ReturnType.IsGenericType && + method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var typeArg = method.ReturnType.GetGenericArguments()[0]; + + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + body = Expression.Call( + ExecuteValueResultTaskOfTMethodInfo.MakeGenericMethod(typeArg), + methodCall, + TargetArg, + HttpContextParameter); + } + else + { + // ExecuteTask(action(..), httpContext); + body = Expression.Call( + ExecuteValueTaskOfTMethodInfo.MakeGenericMethod(typeArg), + methodCall, + TargetArg, + HttpContextParameter); + } + } + else + { + // TODO: Handle custom awaitables + throw new NotSupportedException($"Unsupported return type: {method.ReturnType}"); + } + } + else if (typeof(IResult).IsAssignableFrom(method.ReturnType)) + { + body = Expression.Call(methodCall, ResultWriteResponseAsync, HttpContextParameter); + } + else if (method.ReturnType == typeof(string)) + { + body = Expression.Call(StringResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + } + else + { + body = Expression.Call(JsonResultWriteResponseAsync, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + } + + Func? requestDelegate = null; + + if (consumeBodyDirectly) + { + // We need to generate the code for reading from the body before calling into the delegate + var lambda = Expression.Lambda>(body, TargetArg, HttpContextParameter, DeserializedBodyArg); + var invoker = lambda.Compile(); + object? defaultBodyValue = null; + + if (allowEmptyBody && bodyType!.IsValueType) + { + defaultBodyValue = Activator.CreateInstance(bodyType); + } + + requestDelegate = async (target, httpContext) => + { + object? bodyValue; + + if (allowEmptyBody && httpContext.Request.ContentLength == 0) + { + bodyValue = defaultBodyValue; + } + else + { + try + { + bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType!); + } + catch (IOException ex) + { + Log.RequestBodyIOException(GetLogger(httpContext), ex); + httpContext.Abort(); + return; + } + catch (InvalidDataException ex) + { + Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex); + httpContext.Response.StatusCode = 400; + return; + } + } + + await invoker(target, httpContext, bodyValue); + }; + } + else if (consumeBodyAsForm) + { + var lambda = Expression.Lambda>(body, TargetArg, HttpContextParameter); + var invoker = lambda.Compile(); + + requestDelegate = async (target, httpContext) => + { + // Generating async code would just be insane so if the method needs the form populate it here + // so the within the method it's cached + try + { + await httpContext.Request.ReadFormAsync(); + } + catch (IOException ex) + { + Log.RequestBodyIOException(GetLogger(httpContext), ex); + httpContext.Abort(); + return; + } + catch (InvalidDataException ex) + { + Log.RequestBodyInvalidDataException(GetLogger(httpContext), ex); + httpContext.Response.StatusCode = 400; + return; + } + + await invoker(target, httpContext); + }; + } + else + { + var lambda = Expression.Lambda>(body, TargetArg, HttpContextParameter); + var invoker = lambda.Compile(); + + requestDelegate = invoker; + } + + return httpContext => + { + return requestDelegate(action.Target, httpContext); + }; + } + + private static ILogger GetLogger(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + return loggerFactory.CreateLogger("Microsoft.AspNetCore.Routing.MapAction"); + } + + private static Expression BindParamenter(Expression sourceExpression, ParameterInfo parameter, string? name) + { + var key = name ?? parameter.Name; + var type = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType; + var valueArg = Expression.Convert( + Expression.MakeIndex(sourceExpression, + sourceExpression.Type.GetProperty("Item"), + new[] { Expression.Constant(key) }), + typeof(string)); + + MethodInfo parseMethod = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Static) + let parameters = m.GetParameters() + where m.Name == "Parse" && parameters.Length == 1 && parameters[0].ParameterType == typeof(string) + select m).FirstOrDefault()!; + + Expression? expr = null; + + if (parseMethod != null) + { + expr = Expression.Call(parseMethod, valueArg); + } + else if (parameter.ParameterType != valueArg.Type) + { + // Convert.ChangeType() + expr = Expression.Call(ChangeTypeMethodInfo, valueArg, Expression.Constant(type)); + } + else + { + expr = valueArg; + } + + if (expr.Type != parameter.ParameterType) + { + expr = Expression.Convert(expr, parameter.ParameterType); + } + + // property[key] == null ? default : (ParameterType){Type}.Parse(property[key]); + expr = Expression.Condition( + Expression.Equal(valueArg, Expression.Constant(null)), + Expression.Default(parameter.ParameterType), + expr); + + return expr; + } + + private static MethodInfo GetMethodInfo(Expression expr) + { + var mc = (MethodCallExpression)expr.Body; + return mc.Method; + } + + private static MemberInfo GetMemberInfo(Expression expr) + { + var mc = (MemberExpression)expr.Body; + return mc.Member; + } + + private static async ValueTask ExecuteTask(Task task, HttpContext httpContext) + { + await httpContext.Response.WriteAsJsonAsync(await task); + } + + private static Task ExecuteValueTask(ValueTask task, HttpContext httpContext) + { + static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) + { + await httpContext.Response.WriteAsJsonAsync(await task); + } + + if (task.IsCompletedSuccessfully) + { + return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult()); + } + + return ExecuteAwaited(task, httpContext); + } + + private static Task ExecuteValueTaskResult(ValueTask task, HttpContext httpContext) where T : IResult + { + static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) + { + await (await task).ExecuteAsync(httpContext); + } + + if (task.IsCompletedSuccessfully) + { + return task.GetAwaiter().GetResult().ExecuteAsync(httpContext); + } + + return ExecuteAwaited(task, httpContext); + } + + private static async ValueTask ExecuteTaskResult(Task task, HttpContext httpContext) where T : IResult + { + await (await task).ExecuteAsync(httpContext); + } + + [StackTraceHidden] + private static void ThrowCannotReadBodyDirectlyAndAsForm() + { + throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method."); + } + + private static class Log + { + private static readonly Action _requestBodyIOException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1, "RequestBodyIOException"), + "Reading the request body failed with an IOException."); + + private static readonly Action _requestBodyInvalidDataException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2, "RequestBodyInvalidDataException"), + "Reading the request body failed with an InvalidDataException."); + + public static void RequestBodyIOException(ILogger logger, IOException exception) + { + _requestBodyIOException(logger, exception); + } + + public static void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception) + { + _requestBodyInvalidDataException(logger, exception); + } + } + } +} diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index e96f32ceddf5..67723bd1d175 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -24,6 +24,7 @@ Microsoft.AspNetCore.Routing.RouteCollection + diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index fac586f1b845..adacdfd54759 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -5,9 +5,17 @@ *REMOVED*Microsoft.AspNetCore.Routing.IRouteNameMetadata.RouteName.get -> string! *REMOVED*Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string! *REMOVED*Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string! routeName) -> void +Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder +Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder.Add(System.Action! convention) -> void +Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions Microsoft.AspNetCore.Routing.DataTokensMetadata.DataTokens.get -> System.Collections.Generic.IReadOnlyDictionary! Microsoft.AspNetCore.Routing.DataTokensMetadata.DataTokensMetadata(System.Collections.Generic.IReadOnlyDictionary! dataTokens) -> void Microsoft.AspNetCore.Routing.IDataTokensMetadata.DataTokens.get -> System.Collections.Generic.IReadOnlyDictionary! Microsoft.AspNetCore.Routing.IRouteNameMetadata.RouteName.get -> string? +Microsoft.AspNetCore.Routing.IRouteOrderMetadata +Microsoft.AspNetCore.Routing.IRouteOrderMetadata.RouteOrder.get -> int? +Microsoft.AspNetCore.Routing.IRoutePatternMetadata +Microsoft.AspNetCore.Routing.IRoutePatternMetadata.RoutePattern.get -> string? Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string? Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string? routeName) -> void +static Microsoft.AspNetCore.Builder.MapActionEndpointRouteBuilderExtensions.MapAction(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MapActionEndpointConventionBuilder! diff --git a/src/Http/Routing/test/FunctionalTests/MapActionTest.cs b/src/Http/Routing/test/FunctionalTests/MapActionTest.cs new file mode 100644 index 000000000000..f48d45ee1b3d --- /dev/null +++ b/src/Http/Routing/test/FunctionalTests/MapActionTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.FunctionalTests +{ + public class MapActionTest + { + [Fact] + public async Task MapAction_FromBodyWorksWithJsonPayload() + { + [HttpPost("/EchoTodo/{id}")] + Todo EchoTodo([FromRoute] int id, [FromBody] Todo todo) => todo with { Id = id }; + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(b => b.MapAction((Func)EchoTodo)); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + await host.StartAsync(); + var client = server.CreateClient(); + + var todo = new Todo + { + Name = "Write tests!" + }; + + var response = await client.PostAsJsonAsync("/EchoTodo/42", todo); + response.EnsureSuccessStatusCode(); + + var echoedTodo = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(echoedTodo); + Assert.Equal(todo.Name, echoedTodo?.Name); + Assert.Equal(42, echoedTodo?.Id); + } + + private record Todo + { + public int Id { get; set; } + public string Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } + } + } +} diff --git a/src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj b/src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj index 0fd19466b6b2..0913e1a18394 100644 --- a/src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj +++ b/src/Http/Routing/test/FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Http/Routing/test/UnitTests/Builder/MapActionEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MapActionEndpointRouteBuilderExtensionsTest.cs new file mode 100644 index 000000000000..7ead738d18d0 --- /dev/null +++ b/src/Http/Routing/test/UnitTests/Builder/MapActionEndpointRouteBuilderExtensionsTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Linq; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.TestObjects; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Builder +{ + public class MapActionEndpointDataSourceBuilderExtensionsTest + { + private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); + } + + private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders)); + } + + [Fact] + public void MapAction_BuildsEndpointFromAttributes() + { + const string customPattern = "/CustomTemplate"; + const string customMethod = "CUSTOM_METHOD"; + + [CustomRouteMetadata(Pattern = customPattern, Methods = new[] { customMethod })] + void TestAction() { }; + + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + _ = builder.MapAction((Action)TestAction); + + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal(customPattern, routeEndpointBuilder.RoutePattern.RawText); + + var dataSource = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpMethodMetadata = Assert.Single(endpoint.Metadata.OfType()); + var method = Assert.Single(httpMethodMetadata.HttpMethods); + Assert.Equal(customMethod, method); + } + + [Fact] + public void MapAction_BuildsEndpointWithRouteNameAndOrder() + { + const string customName = "Custom Name"; + const int customOrder = 1337; + + [CustomRouteMetadata(Name = customName, Order = customOrder)] + void TestAction() { }; + + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + _ = builder.MapAction((Action)TestAction); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal(customName, routeEndpointBuilder.DisplayName); + Assert.Equal(customOrder, routeEndpointBuilder.Order); + } + } +} diff --git a/src/Http/Routing/test/UnitTests/Internal/MapActionExpressionTreeBuilderTest.cs b/src/Http/Routing/test/UnitTests/Internal/MapActionExpressionTreeBuilderTest.cs new file mode 100644 index 000000000000..ba06ca795f16 --- /dev/null +++ b/src/Http/Routing/test/UnitTests/Internal/MapActionExpressionTreeBuilderTest.cs @@ -0,0 +1,675 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public class MapActionExpressionTreeBuilderTest + { + [Fact] + public async Task RequestDelegateInvokesAction() + { + var invoked = false; + + void TestAction() + { + invoked = true; + } + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(null!); + + Assert.True(invoked); + } + + [Fact] + public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalRouteParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromRoute] int value) + { + deserializedRouteParam = value; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(originalRouteParam, deserializedRouteParam); + } + + [Fact] + public async Task RequestDelegatePopulatesFromRouteParameterBasedOnAttributeNameProperty() + { + const string specifiedName = "value"; + const int originalRouteParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromRoute(Name = specifiedName)] int foo) + { + deserializedRouteParam = foo; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(originalRouteParam, deserializedRouteParam); + } + + [Fact] + public async Task UsesDefaultValueIfNoMatchingRouteValue() + { + const string unmatchedName = "value"; + const int unmatchedRouteParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromRoute] int foo) + { + deserializedRouteParam = foo; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(0, deserializedRouteParam); + } + + [Fact] + public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalQueryParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromQuery] int value) + { + deserializedRouteParam = value; + } + + var query = new QueryCollection(new Dictionary() + { + [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo) + }); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Query = query; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(originalQueryParam, deserializedRouteParam); + } + + [Fact] + public async Task RequestDelegatePopulatesFromHeaderParameterBasedOnParameterName() + { + const string customHeaderName = "X-Custom-Header"; + const int originalHeaderParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromHeader(Name = customHeaderName)] int value) + { + deserializedRouteParam = value; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(originalHeaderParam, deserializedRouteParam); + } + + [Fact] + public async Task RequestDelegatePopulatesFromBodyParameter() + { + Todo originalTodo = new() + { + Name = "Write more tests!" + }; + + Todo? deserializedRequestBody = null; + + void TestAction([FromBody] Todo todo) + { + deserializedRequestBody = todo; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + httpContext.Request.Body = new MemoryStream(requestBodyBytes); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.NotNull(deserializedRequestBody); + Assert.Equal(originalTodo.Name, deserializedRequestBody!.Name); + } + + [Fact] + public async Task RequestDelegateRejectsEmptyBodyGivenDefaultFromBodyParameter() + { + void TestAction([FromBody] Todo todo) + { + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + } + + [Fact] + public async Task RequestDelegateAllowsEmptyBodyGivenCorrectyConfiguredFromBodyParameter() + { + var todoToBecomeNull = new Todo(); + + void TestAction([FromBody(AllowEmpty = true)] Todo todo) + { + todoToBecomeNull = todo; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Null(todoToBecomeNull); + } + + [Fact] + public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectyConfiguredFromBodyParameter() + { + var structToBeZeroed = new BodyStruct + { + Id = 42 + }; + + void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) + { + structToBeZeroed = bodyStruct; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(default, structToBeZeroed); + } + + [Fact] + public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndAborts() + { + var invoked = false; + + var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); + var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); + + void TestAction([FromBody] Todo todo) + { + invoked = true; + } + + var ioException = new IOException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(testLoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException); + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.True(httpContext.RequestAborted.IsCancellationRequested); + + var logMessage = Assert.Single(sink.Writes); + Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Same(ioException, logMessage.Exception); + } + + [Fact] + public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndSets400Response() + { + var invoked = false; + + var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); + var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); + + void TestAction([FromBody] Todo todo) + { + invoked = true; + } + + var invalidDataException = new InvalidDataException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(testLoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException); + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + + var logMessage = Assert.Single(sink.Writes); + Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Same(invalidDataException, logMessage.Exception); + } + + [Fact] + public async Task RequestDelegatePopulatesFromFormParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalQueryParam = 42; + + int? deserializedRouteParam = null; + + void TestAction([FromForm] int value) + { + deserializedRouteParam = value; + } + + var form = new FormCollection(new Dictionary() + { + [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo) + }); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Form = form; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Equal(originalQueryParam, deserializedRouteParam); + } + + [Fact] + public async Task RequestDelegateLogsFromFormIOExceptionsAsDebugAndAborts() + { + var invoked = false; + + var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); + var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); + + void TestAction([FromForm] int value) + { + invoked = true; + } + + var ioException = new IOException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(testLoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; + httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException); + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.True(httpContext.RequestAborted.IsCancellationRequested); + + var logMessage = Assert.Single(sink.Writes); + Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Same(ioException, logMessage.Exception); + } + + [Fact] + public async Task RequestDelegateLogsFromFormInvalidDataExceptionsAsDebugAndSets400Response() + { + var invoked = false; + + var sink = new TestSink(context => context.LoggerName == "Microsoft.AspNetCore.Routing.MapAction"); + var testLoggerFactory = new TestLoggerFactory(sink, enabled: true); + + void TestAction([FromForm] int value) + { + invoked = true; + } + + var invalidDataException = new InvalidDataException(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(testLoggerFactory); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; + httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException); + httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + + var logMessage = Assert.Single(sink.Writes); + Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Same(invalidDataException, logMessage.Exception); + } + + [Fact] + public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenBothFromBodyAndFromFormOnDifferentParameters() + { + void TestAction([FromBody] int value1, [FromForm] int value2) { } + void TestActionWithFlippedParams([FromForm] int value1, [FromBody] int value2) { } + + Assert.Throws(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction)); + Assert.Throws(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestActionWithFlippedParams)); + } + + [Fact] + public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() + { + void TestAction([FromBody] int value1, [FromBody] int value2) { } + + Assert.Throws(() => MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction)); + } + + [Fact] + public async Task RequestDelegatePopulatesFromServiceParameterBasedOnParameterType() + { + var myOriginalService = new MyService(); + MyService? injectedService = null; + + void TestAction([FromService] MyService myService) + { + injectedService = myService; + } + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(myOriginalService); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Same(myOriginalService, injectedService); + } + + [Fact] + public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() + { + HttpContext? httpContextArgument = null; + + void TestAction(HttpContext httpContext) + { + httpContextArgument = httpContext; + } + + var httpContext = new DefaultHttpContext(); + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Same(httpContext, httpContextArgument); + } + + [Fact] + public async Task RequestDelegatePopulatesIFormCollectionParameterWithoutAttribute() + { + IFormCollection? formCollectionArgument = null; + + void TestAction(IFormCollection httpContext) + { + formCollectionArgument = httpContext; + } + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Action)TestAction); + + await requestDelegate(httpContext); + + Assert.Same(httpContext.Request.Form, formCollectionArgument); + } + + [Fact] + public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody() + { + Todo originalTodo = new() + { + Name = "Write even more tests!" + }; + + Todo TestAction() => originalTodo; + + var httpContext = new DefaultHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Func)TestAction); + + await requestDelegate(httpContext); + + var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions + { + // TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}" + // Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior. + PropertyNameCaseInsensitive = true + }); + + Assert.NotNull(deserializedResponseBody); + Assert.Equal(originalTodo.Name, deserializedResponseBody!.Name); + } + + [Fact] + public async Task RequestDelegateUsesCustomIResult() + { + var resultString = "Still not enough tests!"; + + CustomResult TestAction() => new(resultString!); + + var httpContext = new DefaultHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + var requestDelegate = MapActionExpressionTreeBuilder.BuildRequestDelegate((Func)TestAction); + + await requestDelegate(httpContext); + + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + + Assert.Equal(resultString, decodedResponseBody); + } + + private class Todo + { + public int Id { get; set; } + public string? Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } + } + + private struct BodyStruct + { + public int Id { get; set; } + } + + private class FromRouteAttribute : Attribute, IFromRouteMetadata + { + public string? Name { get; set; } + } + + private class FromQueryAttribute : Attribute, IFromQueryMetadata + { + public string? Name { get; set; } + } + + private class FromHeaderAttribute : Attribute, IFromHeaderMetadata + { + public string? Name { get; set; } + } + + private class FromBodyAttribute : Attribute, IFromBodyMetadata + { + public bool AllowEmpty { get; set; } + } + + private class FromFormAttribute : Attribute, IFromFormMetadata + { + public string? Name { get; set; } + } + + private class FromServiceAttribute : Attribute, IFromServiceMetadata + { + } + + private class MyService + { + } + + private class CustomResult : IResult + { + private readonly string _resultString; + + public CustomResult(string resultString) + { + _resultString = resultString; + } + + public Task ExecuteAsync(HttpContext httpContext) + { + return httpContext.Response.WriteAsync(_resultString); + } + } + + private class IOExceptionThrowingRequestBodyStream : Stream + { + private readonly Exception _exceptionToThrow; + + public IOExceptionThrowingRequestBodyStream(Exception exceptionToThrow) + { + _exceptionToThrow = exceptionToThrow; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw _exceptionToThrow; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } + + private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + { + private readonly CancellationTokenSource _requestAbortedCts = new CancellationTokenSource(); + + public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); } + + public void Abort() + { + _requestAbortedCts.Cancel(); + } + } + } +} diff --git a/src/Http/Routing/test/UnitTests/TestObjects/CustomRouteMetadataAttribute.cs b/src/Http/Routing/test/UnitTests/TestObjects/CustomRouteMetadataAttribute.cs new file mode 100644 index 000000000000..4a2b73ec748f --- /dev/null +++ b/src/Http/Routing/test/UnitTests/TestObjects/CustomRouteMetadataAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Routing.TestObjects +{ + internal class CustomRouteMetadataAttribute : Attribute, IRoutePatternMetadata, IHttpMethodMetadata, IRouteNameMetadata, IRouteOrderMetadata + { + public string Pattern { get; set; } = "/"; + + public string? Name { get; set; } + + public int Order { get; set; } = 0; + + public string[] Methods { get; set; } = new[] { "GET" }; + + string? IRoutePatternMetadata.RoutePattern => Pattern; + + string? IRouteNameMetadata.RouteName => Name; + + int? IRouteOrderMetadata.RouteOrder => Order; + + IReadOnlyList IHttpMethodMetadata.HttpMethods => Methods; + + bool IHttpMethodMetadata.AcceptCorsPreflight => false; + } +} diff --git a/src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs b/src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs index 3c889c23205d..5ba2157094cb 100644 --- a/src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs +++ b/src/Mvc/Mvc.Core/src/AcceptVerbsAttribute.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc { @@ -12,8 +13,10 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies what HTTP methods an action supports. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider + public sealed class AcceptVerbsAttribute : Attribute, IHttpMethodMetadata, IActionHttpMethodProvider, IRouteTemplateProvider { + private readonly List _httpMethods; + private int? _order; /// @@ -35,13 +38,16 @@ public AcceptVerbsAttribute(string method) /// The HTTP methods the action supports. public AcceptVerbsAttribute(params string[] methods) { - HttpMethods = methods.Select(method => method.ToUpperInvariant()); + _httpMethods = methods.Select(method => method.ToUpperInvariant()).ToList(); } /// /// Gets the HTTP methods the action supports. /// - public IEnumerable HttpMethods { get; } + public IEnumerable HttpMethods => _httpMethods; + + IReadOnlyList IHttpMethodMetadata.HttpMethods => _httpMethods; + bool IHttpMethodMetadata.AcceptCorsPreflight => false; /// /// The route template. May be null. @@ -69,4 +75,4 @@ public int Order /// public string Name { get; set; } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs index 9894666c7de9..c3d1b46f3ddc 100644 --- a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Http.Metadata; namespace Microsoft.AspNetCore.Mvc { @@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using the request body. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior + public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior, IFromBodyMetadata { /// public BindingSource BindingSource => BindingSource.Body; @@ -24,5 +25,9 @@ public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEm /// Specifying or will override the framework defaults. /// public EmptyBodyBehavior EmptyBodyBehavior { get; set; } + + // Since the default behavior is to reject empty bodies if MvcOptions.AllowEmptyInputInBodyModelBinding is not configured, + // we'll consider EmptyBodyBehavior.Default the same as EmptyBodyBehavior.Disallow. + bool IFromBodyMetadata.AllowEmpty => EmptyBodyBehavior == EmptyBodyBehavior.Allow; } } diff --git a/src/Mvc/Mvc.Core/src/FromFormAttribute.cs b/src/Mvc/Mvc.Core/src/FromFormAttribute.cs index a42774f17f46..41e7823ea52b 100644 --- a/src/Mvc/Mvc.Core/src/FromFormAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromFormAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using form-data in the request body. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider + public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromFormMetadata { /// public BindingSource BindingSource => BindingSource.Form; diff --git a/src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs b/src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs index 46809454f1cc..c4331287d84a 100644 --- a/src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromHeaderAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using the request headers. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider + public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromHeaderMetadata { /// public BindingSource BindingSource => BindingSource.Header; diff --git a/src/Mvc/Mvc.Core/src/FromQueryAttribute.cs b/src/Mvc/Mvc.Core/src/FromQueryAttribute.cs index df82f67bc322..9f594a27bd9d 100644 --- a/src/Mvc/Mvc.Core/src/FromQueryAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromQueryAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using the request query string. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider + public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata { /// public BindingSource BindingSource => BindingSource.Query; diff --git a/src/Mvc/Mvc.Core/src/FromRouteAttribute.cs b/src/Mvc/Mvc.Core/src/FromRouteAttribute.cs index 94952e502f55..a257d8ffa93e 100644 --- a/src/Mvc/Mvc.Core/src/FromRouteAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromRouteAttribute.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,12 +12,14 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using route-data from the current request. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider + public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromRouteMetadata { /// public BindingSource BindingSource => BindingSource.Path; - /// + /// + /// The name. + /// public string Name { get; set; } } } diff --git a/src/Mvc/Mvc.Core/src/FromServicesAttribute.cs b/src/Mvc/Mvc.Core/src/FromServicesAttribute.cs index 31e299de7373..41c993429383 100644 --- a/src/Mvc/Mvc.Core/src/FromServicesAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromServicesAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -23,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public class FromServicesAttribute : Attribute, IBindingSourceMetadata + public class FromServicesAttribute : Attribute, IBindingSourceMetadata, IFromServiceMetadata { /// public BindingSource BindingSource => BindingSource.Services; diff --git a/src/Mvc/Mvc.Core/src/JsonResult.cs b/src/Mvc/Mvc.Core/src/JsonResult.cs index 6d43cc68323e..d4b23097e969 100644 --- a/src/Mvc/Mvc.Core/src/JsonResult.cs +++ b/src/Mvc/Mvc.Core/src/JsonResult.cs @@ -4,6 +4,7 @@ using System; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; @@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// An action result which formats the given object as JSON. /// - public class JsonResult : ActionResult, IStatusCodeActionResult + public class JsonResult : ActionResult, IResult, IStatusCodeActionResult { /// /// Creates a new with the given . @@ -80,5 +81,15 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = services.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + /// + /// Write the result as JSON to the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + Task IResult.ExecuteAsync(HttpContext httpContext) + { + return httpContext.Response.WriteAsJsonAsync(Value); + } } } diff --git a/src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs b/src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs index 1c204f9cf470..dd0d25b6e4b9 100644 --- a/src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs +++ b/src/Mvc/Mvc.Core/src/Routing/HttpMethodAttribute.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.Routing { @@ -13,8 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// Identifies an action that supports a given set of HTTP methods. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider + public abstract class HttpMethodAttribute : Attribute, IHttpMethodMetadata, IActionHttpMethodProvider, IRouteTemplateProvider { + private readonly List _httpMethods; + private int? _order; /// @@ -40,12 +44,15 @@ public HttpMethodAttribute(IEnumerable httpMethods, string? template) throw new ArgumentNullException(nameof(httpMethods)); } - HttpMethods = httpMethods; + _httpMethods = httpMethods.ToList(); Template = template; } /// - public IEnumerable HttpMethods { get; } + public IEnumerable HttpMethods => _httpMethods; + + IReadOnlyList IHttpMethodMetadata.HttpMethods => _httpMethods; + bool IHttpMethodMetadata.AcceptCorsPreflight => false; /// public string? Template { get; } @@ -68,5 +75,6 @@ public int Order /// [DisallowNull] public string? Name { get; set; } + } } diff --git a/src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs b/src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs index 0934bad99f24..f15e76abc64c 100644 --- a/src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs +++ b/src/Mvc/Mvc.Core/src/Routing/IRouteTemplateProvider.cs @@ -3,12 +3,14 @@ #nullable enable +using Microsoft.AspNetCore.Routing; + namespace Microsoft.AspNetCore.Mvc.Routing { /// /// Interface for attributes which can supply a route template for attribute routing. /// - public interface IRouteTemplateProvider + public interface IRouteTemplateProvider : IRoutePatternMetadata, IRouteOrderMetadata, IRouteNameMetadata { /// /// The route template. May be . @@ -28,5 +30,14 @@ public interface IRouteTemplateProvider /// of relying on selection of a route based on the given set of route values. /// string? Name { get; } + + /// + string? IRoutePatternMetadata.RoutePattern => Template; + + /// + int? IRouteOrderMetadata.RouteOrder => Order; + + /// + string? IRouteNameMetadata.RouteName => Name; } } diff --git a/src/Mvc/Mvc.Core/src/StatusCodeResult.cs b/src/Mvc/Mvc.Core/src/StatusCodeResult.cs index 2f367817bdc0..30d9d84fbf1e 100644 --- a/src/Mvc/Mvc.Core/src/StatusCodeResult.cs +++ b/src/Mvc/Mvc.Core/src/StatusCodeResult.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc /// Represents an that when executed will /// produce an HTTP response with the given response status code. /// - public class StatusCodeResult : ActionResult, IClientErrorActionResult + public class StatusCodeResult : ActionResult, IResult, IClientErrorActionResult { /// /// Initializes a new instance of the class @@ -39,12 +41,28 @@ public override void ExecuteResult(ActionContext context) throw new ArgumentNullException(nameof(context)); } - var factory = context.HttpContext.RequestServices.GetRequiredService(); + Execute(context.HttpContext); + } + + /// + /// Sets the status code on the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + Task IResult.ExecuteAsync(HttpContext httpContext) + { + Execute(httpContext); + return Task.CompletedTask; + } + + private void Execute(HttpContext httpContext) + { + var factory = httpContext.RequestServices.GetRequiredService(); var logger = factory.CreateLogger(); logger.HttpStatusCodeResultExecuting(StatusCode); - context.HttpContext.Response.StatusCode = StatusCode; + httpContext.Response.StatusCode = StatusCode; } } } diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs index f7f9eba93b8a..1b4b93e2686f 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ControllerActionDescriptorProviderTests.cs @@ -304,7 +304,7 @@ public void GetDescriptors_ActionWithHttpMethods_AddedToEndpointMetadata() } [Fact] - public void GetDescriptors_ActionWithMultipleHttpMethods_SingleHttpMethodMetadata() + public void GetDescriptors_ActionWithMultipleHttpMethods_LastHttpMethodMetadata() { // Arrange & Act var descriptors = GetDescriptors( @@ -329,9 +329,9 @@ Action InspectElement(string httpMethod) var httpMethodAttribute = Assert.Single(descriptor.EndpointMetadata.OfType()); Assert.Equal(httpMethod, httpMethodAttribute.HttpMethods.Single(), ignoreCase: true); - var httpMethodMetadata = Assert.Single(descriptor.EndpointMetadata.OfType()); - Assert.Equal(httpMethod, httpMethodMetadata.HttpMethods.Single(), ignoreCase: true); - Assert.False(httpMethodMetadata.AcceptCorsPreflight); + var lastHttpMethodMetadata = descriptor.EndpointMetadata.OfType().Last(); + Assert.Equal(httpMethod, lastHttpMethodMetadata.HttpMethods.Single(), ignoreCase: true); + Assert.False(lastHttpMethodMetadata.AcceptCorsPreflight); }; } } diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index 49b9606749bf..26363dc5d1db 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -1,87 +1,57 @@ -{ +{ "solution": { "path": "..\\..\\AspNetCore.sln", - "projects" : [ - "src\\Mvc\\test\\WebSites\\BasicWebSite\\BasicWebSite.csproj", - "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", - "src\\Mvc\\test\\WebSites\\RazorWebSite\\RazorWebSite.csproj", - "src\\Mvc\\test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj", - "src\\Mvc\\test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj", - "src\\Mvc\\test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj", - "src\\Mvc\\test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj", - "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", - "src\\Mvc\\test\\WebSites\\FilesWebSite\\FilesWebSite.csproj", - "src\\Mvc\\test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj", - "src\\Mvc\\test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj", - "src\\Mvc\\test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj", - "src\\Mvc\\test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj", - "src\\Mvc\\test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj", - "src\\Mvc\\test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj", - "src\\Mvc\\test\\WebSites\\CorsWebSite\\CorsWebSite.csproj", - "src\\Mvc\\samples\\MvcSandbox\\MvcSandbox.csproj", - "src\\Mvc\\test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj", - "src\\Mvc\\test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj", - "src\\Mvc\\test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj", - "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj", - "src\\Mvc\\test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj", - "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj", - "src\\Mvc\\Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj", - "src\\Mvc\\Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj", - "src\\Mvc\\test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj", - "src\\Mvc\\shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", - "src\\Mvc\\Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj", - "src\\Mvc\\Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj", - "src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj", - "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", - "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", + "projects": [ + "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", + "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj", + "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj", + "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj", + "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj", + "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", + "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", + "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", + "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", + "src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj", + "src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj", + "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", + "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", - "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", - "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", - "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", - "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", - "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", - "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", - "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj", - "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", - "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", - "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj", - "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", - "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj", - "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", - "src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj", - "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", - "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", - "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", - "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", - "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", - "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance.Views\\Microsoft.AspNetCore.Mvc.Performance.Views.csproj", - "src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj", - "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", - "src\\Antiforgery\\src\\Microsoft.AspNetCore.Antiforgery.csproj", - "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", - "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", - "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", + "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", + "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj", + "src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj", + "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", - "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", - "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", - "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", - "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", + "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", - "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", - "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", - "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", + "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", + "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", + "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", + "src\\Http\\Routing\\samples\\MapActionSample\\MapActionSample.csproj", + "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", + "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj", + "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj", + "src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.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", "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", - "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", - "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", - "src\\Mvc\\test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj", - "src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj", - "src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj", - "src\\Mvc\\Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj", + "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", + "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", + "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", + "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", + "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj", + "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", + "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj", "src\\Mvc\\Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj", "src\\Mvc\\Mvc.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj", + "src\\Mvc\\Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj", + "src\\Mvc\\Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj", + "src\\Mvc\\Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj", + "src\\Mvc\\Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj", "src\\Mvc\\Mvc.ApiExplorer\\src\\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj", "src\\Mvc\\Mvc.ApiExplorer\\test\\Microsoft.AspNetCore.Mvc.ApiExplorer.Test.csproj", "src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj", @@ -95,44 +65,74 @@ "src\\Mvc\\Mvc.Formatters.Xml\\test\\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test.csproj", "src\\Mvc\\Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj", "src\\Mvc\\Mvc.Localization\\test\\Microsoft.AspNetCore.Mvc.Localization.Test.csproj", - "src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj", - "src\\Mvc\\Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj", + "src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj", + "src\\Mvc\\Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj", + "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj", + "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj", "src\\Mvc\\Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj", "src\\Mvc\\Mvc.RazorPages\\test\\Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj", + "src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj", + "src\\Mvc\\Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj", "src\\Mvc\\Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj", "src\\Mvc\\Mvc.TagHelpers\\test\\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj", + "src\\Mvc\\Mvc.Testing.Tasks\\src\\Microsoft.AspNetCore.Mvc.Testing.Tasks.csproj", + "src\\Mvc\\Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj", "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Mvc\\Mvc.ViewFeatures\\test\\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj", + "src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj", + "src\\Mvc\\Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj", + "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance.Views\\Microsoft.AspNetCore.Mvc.Performance.Views.csproj", + "src\\Mvc\\benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj", + "src\\Mvc\\samples\\MvcSandbox\\MvcSandbox.csproj", + "src\\Mvc\\shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj", + "src\\Mvc\\shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj", + "src\\Mvc\\shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", "src\\Mvc\\test\\Mvc.FunctionalTests\\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj", "src\\Mvc\\test\\Mvc.IntegrationTests\\Microsoft.AspNetCore.Mvc.IntegrationTests.csproj", - "src\\Mvc\\shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj", - "src\\Mvc\\Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj", - "src\\Mvc\\Mvc.Testing.Tasks\\src\\Microsoft.AspNetCore.Mvc.Testing.Tasks.csproj", - "src\\Mvc\\shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj", - "src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj", - "src\\Mvc\\Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj", - "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj", - "src\\Mvc\\Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj", - "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj", - "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", - "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj", - "src\\SignalR\\common\\Http.Connections\\src\\Microsoft.AspNetCore.Http.Connections.csproj", + "src\\Mvc\\test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj", + "src\\Mvc\\test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj", + "src\\Mvc\\test\\WebSites\\BasicWebSite\\BasicWebSite.csproj", + "src\\Mvc\\test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj", + "src\\Mvc\\test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj", + "src\\Mvc\\test\\WebSites\\CorsWebSite\\CorsWebSite.csproj", + "src\\Mvc\\test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj", + "src\\Mvc\\test\\WebSites\\FilesWebSite\\FilesWebSite.csproj", + "src\\Mvc\\test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj", + "src\\Mvc\\test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj", + "src\\Mvc\\test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj", + "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj", + "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj", + "src\\Mvc\\test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj", + "src\\Mvc\\test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj", + "src\\Mvc\\test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj", + "src\\Mvc\\test\\WebSites\\RazorWebSite\\RazorWebSite.csproj", + "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", + "src\\Mvc\\test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj", + "src\\Mvc\\test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj", + "src\\Mvc\\test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj", + "src\\Mvc\\test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj", + "src\\Mvc\\test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj", + "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", + "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", + "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj", + "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj", + "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", + "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", + "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", + "src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj", + "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.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.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", "src\\SignalR\\common\\Http.Connections.Common\\src\\Microsoft.AspNetCore.Http.Connections.Common.csproj", - "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj", - "src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj", - "src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj", + "src\\SignalR\\common\\Http.Connections\\src\\Microsoft.AspNetCore.Http.Connections.csproj", + "src\\SignalR\\common\\Protocols.Json\\src\\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", "src\\SignalR\\common\\Protocols.MessagePack\\src\\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj", "src\\SignalR\\common\\Protocols.NewtonsoftJson\\src\\Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj", - "src\\Mvc\\test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj", - "src\\SignalR\\common\\Protocols.Json\\src\\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", - "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", - "src\\Components\\Authorization\\src\\Microsoft.AspNetCore.Components.Authorization.csproj", - "src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj", - "src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj", - "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", - "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj", - "src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj", - "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj" + "src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj", + "src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj", + "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj" ] } -} +} \ No newline at end of file