Skip to content
代码片段 群组 项目
未验证 提交 77e57843 编辑于 作者: Stephen Halter's avatar Stephen Halter 提交者: GitHub
浏览文件

Remove form support from minimal APIs (#31646)

* Remove form support from minimal APIs

* Add

* MapActionSample -> MinimalSample

* Remove MapActionSample.csproj from Mvc.slnf
上级 00b551ec
No related branches found
No related tags found
无相关合并请求
显示
46 个添加267 个删除
......@@ -1600,8 +1600,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWinFormsApp", "src\Co
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks", "src\Http\Http.Abstractions\perf\Microbenchmarks\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "{3F752B48-2936-4FCA-B0DC-4AB0F788F897}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapActionSample", "src\Http\samples\MapActionSample\MapActionSample.csproj", "{A661D867-708A-494E-8B6B-6558804F9A3F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F0849E7E-61DB-4849-9368-9E7BC125DCB0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsTestApp", "src\Components\WebView\Platforms\WindowsForms\testassets\WinFormsTestApp\WinFormsTestApp.csproj", "{99EE7769-3C81-477B-B947-0A5CBCD5B27D}"
......@@ -1626,6 +1624,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaSer
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaServices.Extensions.Tests", "src\Middleware\Spa\SpaServices.Extensions\test\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", "{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalSample", "src\Http\samples\MinimalSample\MinimalSample.csproj", "{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -7613,18 +7613,6 @@ Global
{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x64.Build.0 = Release|Any CPU
{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.ActiveCfg = Release|Any CPU
{3F752B48-2936-4FCA-B0DC-4AB0F788F897}.Release|x86.Build.0 = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.ActiveCfg = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x64.Build.0 = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.ActiveCfg = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Debug|x86.Build.0 = Debug|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|Any CPU.Build.0 = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.ActiveCfg = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x64.Build.0 = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.ActiveCfg = Release|Any CPU
{A661D867-708A-494E-8B6B-6558804F9A3F}.Release|x86.Build.0 = Release|Any CPU
{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99EE7769-3C81-477B-B947-0A5CBCD5B27D}.Debug|x64.ActiveCfg = Debug|Any CPU
......@@ -7709,6 +7697,18 @@ Global
{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x64.Build.0 = Release|Any CPU
{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x86.ActiveCfg = Release|Any CPU
{AAB50C64-39AA-4AED-8E9C-50D68E7751AD}.Release|x86.Build.0 = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x64.ActiveCfg = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x64.Build.0 = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x86.ActiveCfg = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Debug|x86.Build.0 = Debug|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|Any CPU.Build.0 = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x64.ActiveCfg = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x64.Build.0 = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.ActiveCfg = Release|Any CPU
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -8501,7 +8501,6 @@ Global
{3BA297F8-1CA1-492D-AE64-A60B825D8501} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
{CC740832-D268-47A3-9058-B9054F8397E2} = {D3B76F4E-A980-45BF-AEA1-EA3175B0B5A1}
{3F752B48-2936-4FCA-B0DC-4AB0F788F897} = {DCBBDB52-4A49-4141-8F4D-81C0FFFB7BD5}
{A661D867-708A-494E-8B6B-6558804F9A3F} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
{F0849E7E-61DB-4849-9368-9E7BC125DCB0} = {D4E9A2C5-0838-42DF-BC80-C829C4C9137E}
{99EE7769-3C81-477B-B947-0A5CBCD5B27D} = {F0849E7E-61DB-4849-9368-9E7BC125DCB0}
{94D0D6F3-8632-41DE-908B-47A787D570FF} = {5241CF68-66A0-4724-9BAA-36DB959A5B11}
......@@ -8514,6 +8513,7 @@ Global
{7F99E967-3DC1-4198-9D55-47CD9471D0B6} = {0A064174-8E5C-4F97-B941-A4E302661DF2}
{DF4637DA-5F07-4903-8461-4E2DAB235F3C} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
{AAB50C64-39AA-4AED-8E9C-50D68E7751AD} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
{9647D8B7-4616-4E05-B258-BAD5CAEEDD38} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
......
// 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
{
/// <summary>
/// Interface marking attributes that specify a parameter should be bound using form-data in the request body.
/// </summary>
public interface IFromFormMetadata
{
/// <summary>
/// The form field name.
/// </summary>
string? Name { get; }
}
}
......@@ -8,8 +8,6 @@ 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
......
......@@ -203,48 +203,20 @@ namespace Microsoft.AspNetCore.Http
}
else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
{
if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
if (factoryContext.JsonRequestBodyType is not null)
{
throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
}
if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
{
ThrowCannotReadBodyDirectlyAndAsForm();
}
factoryContext.RequestBodyMode = RequestBodyMode.AsJson;
factoryContext.JsonRequestBodyType = parameter.ParameterType;
factoryContext.AllowEmptyRequestBody = bodyAttribute.AllowEmpty;
return Expression.Convert(BodyValueExpr, parameter.ParameterType);
}
else if (parameterCustomAttributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } formAttribute)
{
if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
{
ThrowCannotReadBodyDirectlyAndAsForm();
}
factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
return BindParameterFromProperty(parameter, FormExpr, formAttribute.Name ?? parameter.Name, factoryContext);
}
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
{
return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
}
else if (parameter.ParameterType == typeof(IFormCollection))
{
if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
{
ThrowCannotReadBodyDirectlyAndAsForm();
}
factoryContext.RequestBodyMode = RequestBodyMode.AsForm;
return Expression.Property(HttpRequestExpr, nameof(HttpRequest.Form));
}
else if (parameter.ParameterType == typeof(HttpContext))
{
return HttpContextExpr;
......@@ -446,67 +418,41 @@ namespace Microsoft.AspNetCore.Http
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext)
{
if (factoryContext.RequestBodyMode is RequestBodyMode.AsJson)
if (factoryContext.JsonRequestBodyType is null)
{
// We need to generate the code for reading from the body before calling into the delegate
var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
var bodyType = factoryContext.JsonRequestBodyType!;
object? defaultBodyValue = null;
if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
{
defaultBodyValue = Activator.CreateInstance(bodyType);
}
return Expression.Lambda<Func<object?, HttpContext, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
}
return async (target, httpContext) =>
{
object? bodyValue;
// We need to generate the code for reading from the body before calling into the delegate
var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0)
{
bodyValue = defaultBodyValue;
}
else
{
try
{
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
}
catch (IOException ex)
{
Log.RequestBodyIOException(httpContext, ex);
return;
}
catch (InvalidDataException ex)
{
Log.RequestBodyInvalidDataException(httpContext, ex);
httpContext.Response.StatusCode = 400;
return;
}
}
var bodyType = factoryContext.JsonRequestBodyType!;
object? defaultBodyValue = null;
await invoker(target, httpContext, bodyValue);
};
if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
{
defaultBodyValue = Activator.CreateInstance(bodyType);
}
else if (factoryContext.RequestBodyMode is RequestBodyMode.AsForm)
return async (target, httpContext) =>
{
var invoker = Expression.Lambda<Func<object?, HttpContext, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
object? bodyValue;
return async (target, httpContext) =>
if (factoryContext.AllowEmptyRequestBody && httpContext.Request.ContentLength == 0)
{
bodyValue = defaultBodyValue;
}
else
{
// 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();
bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType);
}
catch (IOException ex)
{
Log.RequestBodyIOException(httpContext, ex);
httpContext.Abort();
return;
}
catch (InvalidDataException ex)
......@@ -515,15 +461,10 @@ namespace Microsoft.AspNetCore.Http
httpContext.Response.StatusCode = 400;
return;
}
}
await invoker(target, httpContext);
};
}
else
{
return Expression.Lambda<Func<object?, HttpContext, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
}
await invoker(target, httpContext, bodyValue);
};
}
private static MethodInfo GetEnumTryParseMethod()
......@@ -793,22 +734,8 @@ namespace Microsoft.AspNetCore.Http
await (await task).ExecuteAsync(httpContext);
}
[StackTraceHidden]
private static void ThrowCannotReadBodyDirectlyAndAsForm()
{
throw new InvalidOperationException("Action cannot mix FromBody and FromForm on the same method.");
}
private enum RequestBodyMode
{
None,
AsJson,
AsForm,
}
private class FactoryContext
{
public RequestBodyMode RequestBodyMode { get; set; }
public Type? JsonRequestBodyType { get; set; }
public bool AllowEmptyRequestBody { get; set; }
......
......@@ -400,6 +400,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["tryParsable"] = "42";
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
{
["tryParsable"] = "invalid!"
......@@ -422,14 +423,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
void InvalidFromRoute([FromRoute] object notTryParsable) { }
void InvalidFromQuery([FromQuery] object notTryParsable) { }
void InvalidFromHeader([FromHeader] object notTryParsable) { }
void InvalidFromForm([FromForm] object notTryParsable) { }
return new[]
{
new object[] { (Action<object>)InvalidFromRoute },
new object[] { (Action<object>)InvalidFromQuery },
new object[] { (Action<object>)InvalidFromHeader },
new object[] { (Action<object>)InvalidFromForm },
};
}
}
......@@ -700,111 +699,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
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<string, StringValues>()
{
[paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo)
});
var httpContext = new DefaultHttpContext();
httpContext.Request.Form = form;
var requestDelegate = RequestDelegateFactory.Create((Action<int>)TestAction);
await requestDelegate(httpContext);
Assert.Equal(originalQueryParam, deserializedRouteParam);
}
[Fact]
public async Task RequestDelegateLogsFromFormIOExceptionsAsDebugAndAborts()
{
var invoked = false;
void TestAction([FromForm] int value)
{
invoked = true;
}
var ioException = new IOException();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(LoggerFactory);
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException);
httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
var requestDelegate = RequestDelegateFactory.Create((Action<int>)TestAction);
await requestDelegate(httpContext);
Assert.False(invoked);
Assert.True(httpContext.RequestAborted.IsCancellationRequested);
var logMessage = Assert.Single(TestSink.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;
void TestAction([FromForm] int value)
{
invoked = true;
}
var invalidDataException = new InvalidDataException();
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(LoggerFactory);
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException);
httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
var requestDelegate = RequestDelegateFactory.Create((Action<int>)TestAction);
await requestDelegate(httpContext);
Assert.False(invoked);
Assert.False(httpContext.RequestAborted.IsCancellationRequested);
Assert.Equal(400, httpContext.Response.StatusCode);
var logMessage = Assert.Single(TestSink.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<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestAction));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestActionWithFlippedParams));
}
[Fact]
public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
{
......@@ -885,26 +779,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
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 = RequestDelegateFactory.Create((Action<IFormCollection>)TestAction);
await requestDelegate(httpContext);
Assert.Same(httpContext.Request.Form, formCollectionArgument);
}
[Fact]
public async Task RequestDelegatePassHttpContextRequestAbortedAsCancelationToken()
{
......@@ -1180,11 +1054,6 @@ namespace Microsoft.AspNetCore.Routing.Internal
public bool AllowEmpty { get; set; }
}
private class FromFormAttribute : Attribute, IFromFormMetadata
{
public string? Name { get; set; }
}
private class FromServiceAttribute : Attribute, IFromServiceMetadata
{
}
......
......@@ -36,7 +36,7 @@
"src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj",
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
"src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
"src\\Http\\samples\\MapActionSample\\MapActionSample.csproj",
"src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
"src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
"src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
"src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
......
......@@ -22,8 +22,10 @@ using var host = Host.CreateDefaultBuilder(args)
object Json() => new { message = "Hello, World!" };
endpoints.MapGet("/json", (Func<object>)Json);
});
string SayHello(string name) => $"Hello {name}";
endpoints.MapGet("/hello/{name}", (Func<string, string>)SayHello);
});
});
})
.Build();
......
......@@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Specifies that a parameter or property should be bound using form-data in the request body.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromFormMetadata
public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource => BindingSource.Form;
......
......@@ -29,7 +29,6 @@
"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",
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册