.NET Core自定义和默认绑定的合并

16

我正在为一个视图模型创建自定义模型绑定器,实现 IModelBinder

我的视图模型有很多属性,其中大部分不需要任何自定义绑定。我希望能够让框架为我绑定模型,然后再进行任何自定义绑定,而不是从 ModelBindingContext 中逐个显式设置模型的所有属性值:

public class ApplicationViewModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // get .net core to bind values on model

        // Cary out any customization of the models properties

        bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
        return Task.CompletedTask; 
    }
}
基本上,我想执行默认模型绑定,然后应用自定义绑定,类似于此SO帖子中采用的方法,但针对的是.NET Core而不是框架。
我认为应用默认绑定应该很简单,但是我一直没有找到如何做到这一点。我相信解决方案涉及到 ComplexTypeModelBinder ComplexTypeModelBinderProvider 类,但似乎找不到如何去做。
我知道当POST请求到达我的控制器方法时,我可以进行任何更改,但这似乎是错误的时间和地点。
2个回答

8

如果需要自定义ComplexTypeModelBinder,可以继承ComplexTypeModelBinder

  1. 模型
    public class BinderModel
    {
       public int Id { get; set; }
       public string Name { get; set; }
       public string BinderValue { get; set; }
    }
  1. 控制器操作
    [HttpPost]
    public void Post([FromForm]BinderModel value)
    {

    }
  1. 自定义绑定器
    public class CustomBinder : ComplexTypeModelBinder
    {
        private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
        public CustomBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
        : base(propertyBinders)
        {
            _propertyBinders = propertyBinders;
        }
        protected override Task BindProperty(ModelBindingContext bindingContext)
        {
            if (bindingContext.FieldName == "BinderValue")
            {
                bindingContext.Result = ModelBindingResult.Success("BinderValueTest");
                return Task.CompletedTask;
            }
            else
            {
                return base.BindProperty(bindingContext);
            }
        }
        protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
        {
            base.SetProperty(bindingContext, modelName, propertyMetadata, result);
        }
    }
  1. CustomBinderProvider
    public class CustomBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
            {
                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                for (var i = 0; i < context.Metadata.Properties.Count; i++)
                {
                    var property = context.Metadata.Properties[i];
                    propertyBinders.Add(property, context.CreateBinder(property));
                }

                //var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
                //return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
                return new CustomBinder(propertyBinders);
            }

            return null;
        }

    }
  1. 注入提供者
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options => {
            options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
        });
    }

1
ComplexTypeModelBinder没有单参数构造函数。我使用了IModelBinder。 - tnJed

3

ComplexTypeModelBinder在 .Net 5.0 中已被弃用,而它的对应项ComplexObjectModelBinder是sealed的,因此您不能从中扩展。

但是,您可以绕过这个问题。ComplexObjectModelBinderProvider是公开的,您可以使用它来创建一个ComplexObjectModelBinder。因此,如果您自己制作一个自定义的IModelBinderProvider,您可以使构造函数接受一个ComplexObjectModelBinderProvider参数,并利用它来创建一个ComplexObjectModelBinder。然后,您可以将其传递给您的自定义IModelBinder,在执行自定义工作之前,它会愉快地退回到您提供的ComplexObjectModelBinder

这里有一个例子。首先,是您的IModelBinder。如果需要的话,您可以使用DI。(在此示例中,假设我们需要一个DbContext。)

public class MyCustomModelBinder : IModelBinder
{
    private readonly IModelBinder _defaultBinder;
    private readonly DbContext _dbContext;
    public MyCustomModelBinder(IModelBinder defaultBinder, DbContext dbContext)
    {
        _defaultBinder = defaultBinder;
        _dbContext = dbContext;
    }

    public override Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // -do custom work here-

        return _defaultBinder.BindModelAsync(bindingContext);
    }
}

然而,为了在自定义模型绑定器上使用DI,您需要一个帮助类。问题是,当调用 IModelBinderProvider 时,它将无法访问典型请求中的所有服务,例如您的 DbContext。但是,这个类可以帮助解决问题:

internal class DIModelBinder : IModelBinder
{
    private readonly IModelBinder _rootBinder;
    private readonly ObjectFactory _factory;

    public DIModelBinder(Type binderType, IModelBinder rootBinder)
    {
        if (!typeof(IModelBinder).IsAssignableFrom(binderType))
        {
            throw new ArgumentException($"Your binderType must derive from IModelBinder.");
        }
        
        _factory = ActivatorUtilities.CreateFactory(binderType, new[] { typeof(IModelBinder) });
        _rootBinder = rootBinder;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var requestServices = bindingContext.HttpContext.RequestServices;
        var binder = (IModelBinder)_factory(requestServices, new[] { _rootBinder });
        return binder.BindModelAsync(bindingContext);
    }
}

现在,你已经准备好编写自定义的 IModelBinderProvider 了:
public class MyCustomModelBinderProvider : IModelBinderProvider
{
    private readonly IModelBinderProvider _rootProvider;
    public MyCustomModelBinderProvider(IModelBinderProvider rootProvider)
    {
        _rootProvider = rootProvider;
    }
    
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(MyModel))
        {
            var rootBinder = _rootProvider.GetBinder(context)
                ?? throw new InvalidOperationException($"Root {_rootProvider.GetType()} did not provide an IModelBinder for MyModel.");

            return new DIModelBinder(typeof(MyCustomModelBinder), rootBinder);
        }

        return null;
    }
}

最后,在您配置服务的启动文件中,您可以获取ComplexObjectModelBinderProvider实例,使用它来创建一个新的MyCustomModelBinderProvider实例,并将其插入到ModelBinderProviders中。

services.AddMvc(options =>
{
    var fallbackProvider = options.ModelBinderProviders
        .First(p => p is ComplexObjectModelBinderProvider);
    var myProvider = new MyCustomModelBinderProvider(fallbackProvider);
    options.ModelBinderProviders.Insert(0, myProvider);
})

感谢您提供详细的答案。我发现在我的自定义绑定工作之后运行默认绑定器会覆盖它。我可以在之前运行默认绑定器,但这意味着如果它尝试解析由于我需要进行自定义绑定而无法处理的必需字段,则会出现错误。有没有办法防止默认绑定器覆盖我设置的值? - Hazza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接