【问题标题】:How to implement custom controller action selection in ASP.Net Core?如何在 ASP.Net Core 中实现自定义控制器动作选择?
【发布时间】:2021-06-18 05:17:36
【问题描述】:

我有一个 ASP.Net Core API 项目。我希望能够编写自定义路由逻辑,以便能够根据 HTTP Body 参数选择不同的控制器操作。为了说明我的问题,这是我的Controller 课程:

[ApiController]
[Route("api/[controller]")]
public class TestController
{
    // should be called when HTTP Body contains json: '{ method: "SetValue1" }'
    public void SetValue1()
    {
        // some logic
    }

    // should be called when HTTP Body contains json: '{ method: "SetValue2" }'
    public void SetValue2()
    {
        // some logic
    }
}

从我的 cmets 中可以看出,我想根据 HTTP 正文选择不同的操作方法。 这是我的Startup 课程:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // I assume instead of this built in routing middleware, 
        // I will have to implement custom middleware to choose the correct endpoints, from HTTP body,
        // any ideas how I can go about this?
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

我可以使用的选项之一是使用一个条目 Action 方法,该方法将根据 HTTP 正文内容调用不同的方法,但我想避免这种情况,并将此逻辑封装在自定义路由中的某个位置。

在旧的APS.Net Web API 中有一个方便的类ApiControllerActionSelector,我可以扩展它,并定义我选择操作方法的自定义逻辑,但是新的ASP.Net Core 不支持这个。我想我将不得不实现我自己的app.UseRouting 中间件版本。关于如何做到这一点的任何想法?

【问题讨论】:

  • 如果你决定执行哪个动作,那么这意味着调用者并不关心执行哪个动作,只要工作完成,对吗?如果这对你来说没问题,我建议采用不同的方法 - 创建一个调用服务的单个操作,该服务决定它处理什么样的请求,例如通过读取请求类型的字段。如果支持请求类型,请为其创建处理程序并执行处理程序提供的逻辑。我在与第三方 Webhook 集成时使用此技术。一个端点用于多种类型的事件。

标签: asp.net asp.net-core .net-core asp.net-core-webapi asp.net-mvc-routing


【解决方案1】:

在旧的asp.net core3.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.SelectBestCandidateIActionSelector 方法,它提供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>();

【讨论】:

  • 感谢您帮助我!似乎ActionSelector 类在ASP.Net Core 5.0 中不可用。我稍微修改了您的代码,但没有从 ActionSelector 继承。我确实在我的Startup 类中注册了RequestBodyActionSelector,但不幸的是,由于某种原因它从未被调用,控制器和动作被正常选择。我缺少任何特定的设置吗?您能否将StartupController 课程代码添加到您的答案中?如果它符合ASP.Net Core 5.0,我也将不胜感激
  • @MykhailoSeniutovych 您是否按照答案末尾的指导注册了IActionSelector?我对.net 5 不是很熟悉,但看起来.net 5 中的路由有点不同,我也会尝试在Startup.Configure 中添加这个app.UseRouting()
  • @MykhailoSeniutovych 我刚刚使用端点路由的解决方案更新了答案(实际上它在asp.net core 2.2 中部分引入,可以通过设置MvcOptions.EnableEndpointRouting = true 启用)。这就是为什么我可以继续编写和测试它。
  • 我测试了2.2 代码并且它有效,所以我会接受答案。不幸的是,带有EndpointSelector 的新代码在ASP.Net Core 3.1 及更高版本中不起作用,因为您在那里使用EndpointSelectorContext 类,而这些新版本中不存在该类。
  • @MykhailoSeniutovych 感谢您的回复,实际上我无法在.net core 3.1 上对其进行测试,我认为自 2.2 以来设计保持不变。实际上代码几乎相同,EndpointSelectorContext 中的RouteValues 被移动到HttpRequest.RouteValues 并且Endpoint 可以使用HttpContext 上的扩展方法来获取/设置。我刚刚使用自定义EndpointSelector 更新了代码,当然我无法测试它。希望你可以自己测试一下。如果可能,您应该使用较新版本的asp.net core
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-10
  • 1970-01-01
  • 2020-07-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多