如何在 MVC 6 中编写多态模型绑定器和提供程序

use*_*312 5 polymorphism asp.net-mvc modelbinder

这个问题之前已经在 SO 和其他地方在 MVC3 的上下文中被问过,并且有一些与 ASP.NET Core RC1 和 RC2 相关的点点滴滴,但没有一个例子来实际展示如何在 MVC 6 中以正确的方式做到这一点.

有以下类

 public abstract class BankAccountTransactionModel {

    public long Id { get; set; }
    public DateTime Date { get; set; }
    public decimal Amount { get; set; }
    public readonly string ModelType;
    public BankAccountTransactionModel(string modelType) {
        this.ModelType = modelType;
    }
 }

 public class BankAccountTransactionModel1 : BankAccountTransactionModel{

    public bool IsPending { get; set; }

    public BankAccountTransactionModel1():
            base(nameof(BankAccountTransactionModel1)) {}
 }    

 public class BankAccountTransactionModel2 : BankAccountTransactionModel{

    public bool IsPending { get; set; }

    public BankAccountTransactionModel2():
            base(nameof(BankAccountTransactionModel2)) {}
 }
Run Code Online (Sandbox Code Playgroud)

在我的控制器中,我有这样的东西

[Route(".../api/[controller]")]  
public class BankAccountTransactionsController : ApiBaseController 
{

 [HttpPost]
 public IActionResult Post(BankAccountTransactionModel model) {

        try {
            if (model == null || !ModelState.IsValid) {
                // failed to bind the model
                return BadRequest(ModelState);
            }

            this.bankAccountTransactionRepository.SaveTransaction(model);

            return this.CreatedAtRoute(ROUTE_NAME_GET_ITEM, new { id = model.Id }, model);
        } catch (Exception e) {

            this.logger.LogError(LoggingEvents.POST_ITEM, e, string.Empty, null);
            return StatusCode(500);
        }
    }

}        
Run Code Online (Sandbox Code Playgroud)

我的客户可能会发布 BankAccountTransactionModel1 或 BankAccountTransactionModel2,我想使用自定义模型绑定器根据抽象基类 BankAccountTransactionModel 上定义的属性 ModelType 中的值来确定要绑定的具体模型。

因此我做了以下工作

1) 编写了一个简单的 Model Binder Provider,用于检查类型是否为 BankAccountTransactionModel。如果是这种情况,则返回 BankAccountTransactionModelBinder 的一个实例。

public class BankAccountTransactionModelBinderProvider : IModelBinderProvider {

    public IModelBinder GetBinder(ModelBinderProviderContext context) {

        if (context == null) throw new ArgumentNullException(nameof(context));

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) {     

            var type1 = context.Metadata.ModelType;
            var type2 = typeof(BankAccountTransactionModel);

            // some other code here?

            // tried this but not sure what to do with it!
            foreach (var property in context.Metadata.Properties) {
                propertyBinders.Add(property, context.CreateBinder(property));
            }

            if (type1 == type2) {
                return new BankAccountTransactionModelBinder(propertyBinders);
            }
        }

        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

2) 编写了 BankAccountTransactionModel

 public class BankAccountTransactionModelBinder : IModelBinder {


 private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;

 public BankAccountTransactionModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders){
                this._propertyBinders = propertyBinders;
                  }
 public Task BindModelAsync(ModelBindingContext bindingContext) {

        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));            

        // I would like to be able to read the value of the property
        // ModelType like this or in some way...
        // This does not work and typeValue is...
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");

        // then once I know whether it is a Model1 or Model2 I would like to
        // instantiate one and get the values from the body of the Http  
        // request into the properties of the instance  

        var model = Activator.CreateInstance(type);

        // read the body of the request in some way and set the 
        // properties of model                                                                                                           

        var key = some key? 
        var result = ModelBindingResult.Success(key, model);

        // Job done  
        return Task.FromResult(result);
    }

 }
Run Code Online (Sandbox Code Playgroud)

3)最后我在 Startup.cs 中注册了提供者

public void ConfigureServices(IServiceCollection services)
    {       
        services.AddMvc(options => {
            options.ModelBinderProviders.Insert(0, new BankAccountTransactionModelBinderProvider());
            options.Filters.Add(typeof (SetUserContextAttribute));
        });
Run Code Online (Sandbox Code Playgroud)

整个事情看起来没问题,因为提供者实际上被调用了,模型构建器也是如此。但是,我似乎无法在模型绑定器的 BindModelAsync 中对逻辑进行编码。

正如代码中的注释所述,我想要在我的模型绑定器中做的就是从 http 请求的正文中读取,特别是我的 JSON 中 ModelType 的值。然后在此基础上,我想实例化 BankAccountTransactionModel1 或 BankAccountTransactionModel 并最终通过读取主体中的 JSON 将值分配给此实例的属性。

我知道这只是应该如何完成的粗略近似,但我将不胜感激一些帮助以及如何做到或已经完成的示例。

我遇到过以下示例,其中 ModelBinder 中的以下代码行

var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
Run Code Online (Sandbox Code Playgroud)

应该读取值。但是,它在我的模型绑定器中不起作用,并且 typeValue 始终如下所示

typeValue
{}
Culture: {}
FirstValue: null
Length: 0
Values: {}
Results View: Expanding the Results View will enumerate the IEnumerable
Run Code Online (Sandbox Code Playgroud)

我也注意到

bindingContext.ValueProvider
Count = 2
[0]: {Microsoft.AspNetCore.Mvc.ModelBinding.RouteValueProvider}
[1]: {Microsoft.AspNetCore.Mvc.ModelBinding.QueryStringValueProvider}
Run Code Online (Sandbox Code Playgroud)

这可能意味着我没有机会从身体上读取任何内容。

我是否可能需要在混合中使用“格式化程序”才能获得所需的结果?

某个地方是否已经存在类似自定义模型绑定器的参考实现,以便我可以简单地使用它,也许还有一些简单的 mod?

谢谢你。