如何在 ASP.Net Core 中实现自定义控制器动作选择?

如何解决如何在 ASP.Net Core 中实现自定义控制器动作选择?

我有一个 ASP.Net Core API 项目。我希望能够编写自定义路由逻辑,以便能够根据 HTTP 正文参数选择不同的控制器操作。为了说明我的问题,这是我的 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
    }
}

从我的评论中可以看出,我想根据 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,我可以扩展它,并定义我选择 Action 方法自定义逻辑,但是在新的 ASP.Net Core不支持。我想我必须实现我自己的 app.UseRouting 中间件版本。关于如何做到这一点的任何想法?

解决方法

在旧的 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 两次(当所选操作方法有一些模型绑定参数时)。即使框架在 EnableBuffering 上支持所谓的 HttpRequest,接受它仍然有点权衡。其次,在选择最佳动作的方法中(定义在 IActionSelector 上),我们不能使用 async,因此读取请求正文当然不能用 async 完成。

对于高性能网络应用程序,绝对应该避免这种情况。但是,如果您可以接受这种权衡,我们可以通过实现自定义 IActionSelector 来提供解决方案,最多让它继承默认 ActionSelector。我们可以覆盖的方法是 ActionSelector.SelectBestActions。但是,该方法不提供 RouteContext(我们需要访问它来更新 RouteData),因此我们将重新实现名为 IActionSelectorIActionSelector.SelectBestCandidate 的另一个方法提供一个 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);
    }        
}

要在 IActionSelector 中注册自定义 Startup.ConfigureServices

services.AddSingleton<IActionSelector,RequestBodyActionSelector>();

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?