在给定 HttpContext 的情况下,从 AuthorizationHandler 为查询参数调用默认模型联编程序

Invoking default model binder for query parameters from an AuthorizationHandler, given a HttpContext

问题

在 ASP.NET Core 2.2 中,我正在实现一个 AuthorizationHandler(我可以在其中访问传入请求的 HttpContext)。

如何从 AuthorizationHandler 中为 MVC 使用的查询参数调用相同的模型绑定器?

理想情况下,我想编写一个扩展方法,允许我编写如下内容:

HttpContext.Request.Query.BindValue<ICollection<MembershipType>>("membershipType");

returns ICollection<MembershipType> 的新实例基于名为 "membershipType" 的查询参数(其中 MembershipType 是一个枚举)。

背景

假设我有一个控制器操作,其中执行查询参数的复杂模型绑定,例如像这样的东西(MembershipType 是一个枚举):

[HttpGet]
[Authorize("MyPolicyThatUsesMyRequirement")]
public Task<ActionResult<List<Member>>> GetMembers([FromQuery] ICollection<MembershipType> membershipTypeFilter = null)
{
    // skipping actual implementation
}

现在我想创建一个 AuthorizationHandler,它只对 membershipTypeFilter 条目和用户角色的某些组合成功。这意味着在我的 AuthorizationHandler 中,我需要访问 membershipTypeFilter 参数。

让我们看一下 AuthorizationHandler 的以下框架:

class MyRequirement : AuthorizationHandler<MyRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement)
    {
        var mvcContext = context.Resource as AuthorizationFilterContext;

        // TODO: need access to query parameters here

        return Task.CompletedTask;
    }
}

mvcContext.HttpContext.Request.Query 让我可以访问查询参数,但只能以字符串数组字典的形式。当然,我可以解析该字符串集合并以某种方式将其转换为枚举值集合。但在这种情况下,以与 MVC 调用我的控制器操作时完全相同的方式完成此转换至关重要。所以我正在寻找一种方法来调用与 MVC 使用的相同的模型绑定器。

对于MVC模型绑定,由Bind实现。您需要在自己的代码中使用它。

在我看来,这对您的要求来说太过载了,对您来说更好的选择是直接从查询字符串中获取值。

var parameters = mvcContext
                        .ActionDescriptor
                        .Parameters
                        .Select(s => new
                        {
                            Name = s.Name,
                            Value = mvcContext.HttpContext.Request.Query[s.Name]
                        });

如果您想在 AuthorizationHandler 中尝试使用 MVC 模型绑定,请尝试

public class MyNewRequirement : AuthorizationHandler<MyRequirement>, IAuthorizationRequirement
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement)
    {
        var mvcContext = context.Resource as AuthorizationFilterContext;
        //required service
        var _mvcOptions = mvcContext.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
        var parameterBinder = mvcContext.HttpContext.RequestServices.GetRequiredService<ParameterBinder>();
        var _modelBinderFactory = mvcContext.HttpContext.RequestServices.GetRequiredService<IModelBinderFactory>();
        var _modelMetadataProvider = mvcContext.HttpContext.RequestServices.GetRequiredService<IModelMetadataProvider>();

        var controllerContext = new ControllerContext(mvcContext);
        controllerContext.ValueProviderFactories = new CopyOnWriteList<IValueProviderFactory>(_mvcOptions.ValueProviderFactories.ToArray());
        var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
        var parameters = controllerContext.ActionDescriptor.Parameters;
        var parameterBindingInfo = GetParameterBindingInfo(
                        _modelBinderFactory,
                        _modelMetadataProvider,
                        controllerContext.ActionDescriptor,
                        _mvcOptions);

        for (var i = 0; i < parameters.Count; i++)
        {
            var parameter = parameters[i];
            var bindingInfo = parameterBindingInfo[i];
            var modelMetadata = bindingInfo.ModelMetadata;

            if (!modelMetadata.IsBindingAllowed)
            {
                continue;
            }

            var model = await parameterBinder.BindModelAsync(
                controllerContext,
                bindingInfo.ModelBinder,
                valueProvider,
                parameter,
                modelMetadata,
                value: null);
        }          

    }
    private static BinderItem[] GetParameterBindingInfo(
        IModelBinderFactory modelBinderFactory,
        IModelMetadataProvider modelMetadataProvider,
        ControllerActionDescriptor actionDescriptor,
        MvcOptions mvcOptions)
        {
            var parameters = actionDescriptor.Parameters;
            if (parameters.Count == 0)
            {
                return null;
            }

            var parameterBindingInfo = new BinderItem[parameters.Count];
            for (var i = 0; i < parameters.Count; i++)
            {
                var parameter = parameters[i];

                ModelMetadata metadata;
                if (mvcOptions.AllowValidatingTopLevelNodes &&
                    modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase &&
                    parameter is ControllerParameterDescriptor controllerParameterDescriptor)
                {
                    // The default model metadata provider derives from ModelMetadataProvider
                    // and can therefore supply information about attributes applied to parameters.
                    metadata = modelMetadataProviderBase.GetMetadataForParameter(controllerParameterDescriptor.ParameterInfo);
                }
                else
                {
                    // For backward compatibility, if there's a custom model metadata provider that
                    // only implements the older IModelMetadataProvider interface, access the more
                    // limited metadata information it supplies. In this scenario, validation attributes
                    // are not supported on parameters.
                    metadata = modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
                }

                var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
                {
                    BindingInfo = parameter.BindingInfo,
                    Metadata = metadata,
                    CacheToken = parameter,
                });

                parameterBindingInfo[i] = new BinderItem(binder, metadata);
            }

            return parameterBindingInfo;
        }
    private struct BinderItem
    {
        public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata)
        {
            ModelBinder = modelBinder;
            ModelMetadata = modelMetadata;
        }

        public IModelBinder ModelBinder { get; }

        public ModelMetadata ModelMetadata { get; }
    }

}