在旧的asp.net core(3.0 之前)中,我们可以实现自定义的IActionSelector,并且在ActionSelector 仍然公开的情况下特别方便。但是随着新的端点路由,它变成了所谓的EndpointSelector。实现是完全一样的,关键是我们如何提取ActionDescriptor,它作为元数据放在Endpoint 中。以下实现需要默认的EndpointSelector(即DefaultEndpointSelector),但不幸的是这是内部的。因此,我们需要使用一种技巧来获取该默认实现的实例,以便在我们的自定义实现中使用。
public class RequestBodyEndpointSelector : EndpointSelector
{
readonly IEnumerable<Endpoint> _controllerEndPoints;
readonly EndpointSelector _defaultSelector;
public RequestBodyEndpointSelector(EndpointSelector defaultSelector, EndpointDataSource endpointDataSource)
{
_defaultSelector = defaultSelector;
_controllerEndPoints = endpointDataSource.Endpoints
.Where(e => e.Metadata.GetMetadata<ControllerActionDescriptor>() != null).ToList();
}
public override async Task SelectAsync(HttpContext httpContext, CandidateSet candidates)
{
var request = httpContext.Request;
request.EnableBuffering();
//don't use "using" here, otherwise the request.Body will be disposed and cannot be used later in the pipeline (an exception will be thrown).
var sr = new StreamReader(request.Body);
try
{
var body = sr.ReadToEnd();
if (!string.IsNullOrEmpty(body))
{
try
{
var actionInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<ActionInfo>(body);
var controllerActions = new HashSet<(MethodInfo method, Endpoint endpoint, RouteValueDictionary routeValues, int score)>();
var constrainedControllerTypes = new HashSet<Type>();
var routeValues = new List<RouteValueDictionary>();
var validIndices = new HashSet<int>();
for (var i = 0; i < candidates.Count; i++)
{
var candidate = candidates[i];
var endpoint = candidates[i].Endpoint;
var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
if (actionDescriptor == null) continue;
routeValues.Add(candidate.Values);
constrainedControllerTypes.Add(actionDescriptor.MethodInfo.DeclaringType);
if (!string.Equals(actionInfo.MethodName, actionDescriptor.MethodInfo.Name,
StringComparison.OrdinalIgnoreCase)) continue;
if (!controllerActions.Add((actionDescriptor.MethodInfo, endpoint, candidate.Values, candidate.Score))) continue;
validIndices.Add(i);
}
if (controllerActions.Count == 0)
{
var bestCandidates = _controllerEndPoints.Where(e => string.Equals(actionInfo.MethodName,
e.Metadata.GetMetadata<ControllerActionDescriptor>().MethodInfo.Name,
StringComparison.OrdinalIgnoreCase)).ToArray();
var routeValuesArray = request.RouteValues == null ? routeValues.ToArray() : new[] { request.RouteValues };
candidates = new CandidateSet(bestCandidates, routeValuesArray, new[] { 0 });
}
else
{
for(var i = 0; i < candidates.Count; i++)
{
candidates.SetValidity(i, validIndices.Contains(i));
}
}
//call the default selector after narrowing down the candidates
await _defaultSelector.SelectAsync(httpContext, candidates);
//if some endpoint found
var selectedEndpoint = httpContext.GetEndpoint();
if (selectedEndpoint != null)
{
//update the action in the RouteData to found endpoint
request.RouteValues["action"] = selectedEndpoint.Metadata.GetMetadata<ControllerActionDescriptor>().ActionName;
}
return;
}
catch { }
}
}
finally
{
request.Body.Position = 0;
}
await _defaultSelector.SelectAsync(httpContext, candidates);
}
}
注册码是这样的有点棘手:
//define an extension method for registering conveniently
public static class EndpointSelectorServiceCollectionExtensions
{
public static IServiceCollection AddRequestBodyEndpointSelector(this IServiceCollection services)
{
//build a dummy service container to get an instance of
//the DefaultEndpointSelector
var sc = new ServiceCollection();
sc.AddMvc();
var defaultEndpointSelector = sc.BuildServiceProvider().GetRequiredService<EndpointSelector>();
return services.Replace(new ServiceDescriptor(typeof(EndpointSelector),
sp => new RequestBodyEndpointSelector(defaultEndpointSelector,
sp.GetRequiredService<EndpointDataSource>()),
ServiceLifetime.Singleton));
}
}
//inside the Startup.ConfigureServices
services.AddRequestBodyEndpointSelector();
asp.net core 2.2中使用的旧常规路由的旧解决方案
您的要求有点奇怪,您可能需要为此做出一些权衡。首先,该要求可能要求您阅读Request.Body 两次(当所选操作方法具有模型绑定的一些参数时)。即使框架在HttpRequest 上支持所谓的EnableBuffering,仍然需要权衡取舍。其次在选择最佳动作的方法中(定义在IActionSelector上),我们不能使用async所以读取请求体当然不能用async来完成。
对于高性能网络应用程序,绝对应该避免这种情况。但是如果你能接受这种权衡,我们有一个解决方案,通过实现自定义IActionSelector,最好让它继承默认的ActionSelector。我们可以覆盖的方法是ActionSelector.SelectBestActions。然而,该方法不提供RouteContext(我们需要访问它来更新RouteData),因此我们将重新实现另一个名为IActionSelector.SelectBestCandidate 的IActionSelector 方法,它提供RouteContext。
详细代码如下:
//first we define a base model for parsing the request body
public class ActionInfo
{
[JsonProperty("method")]
public string MethodName { get; set; }
}
//here's our custom ActionSelector
public class RequestBodyActionSelector : ActionSelector, IActionSelector
{
readonly IEnumerable<ActionDescriptor> _actions;
public RequestBodyActionSelector(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
ActionConstraintCache actionConstraintCache, ILoggerFactory loggerFactory)
: base(actionDescriptorCollectionProvider, actionConstraintCache, loggerFactory)
{
_actions = actionDescriptorCollectionProvider.ActionDescriptors.Items;
}
ActionDescriptor IActionSelector.SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
{
var request = context.HttpContext.Request;
//supports reading the request body multiple times
request.EnableBuffering();
var sr = new StreamReader(request.Body);
try
{
var body = sr.ReadToEnd();
if (!string.IsNullOrEmpty(body))
{
try
{
//here I use the old Newtonsoft.Json
var actionInfo = JsonConvert.DeserializeObject<ActionInfo>(body);
//the best actions should be on these controller types.
var controllerTypes = new HashSet<TypeInfo>(candidates.OfType<ControllerActionDescriptor>().Select(e => e.ControllerTypeInfo));
//filter for the best by matching the controller types and
//the method name from the request body
var bestActions = _actions.Where(e => e is ControllerActionDescriptor ca &&
controllerTypes.Contains(ca.ControllerTypeInfo) &&
string.Equals(actionInfo.MethodName, ca.MethodInfo.Name, StringComparison.OrdinalIgnoreCase)).ToList();
//only override the default if any method name matched
if (bestActions.Count > 0)
{
//before reaching here,
//the RouteData has already been populated with
//what from the request's URL
//By overriding this way, that RouteData's action
//may be changed, so we need to update it here.
var newActionName = (bestActions[0] as ControllerActionDescriptor).ActionName;
context.RouteData.PushState(null, new RouteValueDictionary(new { action = newActionName }), null);
return SelectBestCandidate(context, bestActions);
}
}
catch { }
}
}
finally
{
request.Body.Position = 0;
}
return SelectBestCandidate(context, candidates);
}
}
在Startup.ConfigureServices中注册自定义IActionSelector:
services.AddSingleton<IActionSelector, RequestBodyActionSelector>();