5

This question has been asked before on SO and elsewhere in the context of MVC3 and there are bits and bobs about it related to ASP.NET Core RC1 and RC2 but niot a single example that actually shows how to do it the right way in MVC 6.

There are the following classes

 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)) {}
 }

In my controller I have something like this

[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);
        }
    }

}        

My client may post either BankAccountTransactionModel1 or BankAccountTransactionModel2 and I would like to use a custom model binder to determine which concrete model to bind based on the value in the property ModelType which is defined on the abstract base class BankAccountTransactionModel.

Thus I have done the following

1) Coded up a simple Model Binder Provider that checks that the type is BankAccountTransactionModel. If this is the case then an instance of BankAccountTransactionModelBinder is returned.

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;
    }
}

2) Coded up the 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);
    }

 }

3) Lastly I register the provider in Startup.cs

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

The whole thing seems OK in that the provider is actually invoked and the same is the case for the model builder. However, I cannot seem to get anywhere with coding the logic in BindModelAsync of the model binder.

As already stated by the comments in the code, all that I'd like to do in my model binder is to read from the body of the http request and in particular the value of ModelType in my JSON. Then on the bases of that I'd like to instantiate either BankAccountTransactionModel1 or BankAccountTransactionModel and finally assign values to the property of this instance by reading them of the JSON in the body.

I know that this is a only a gross approximation of how it should be done but I would greatly appreciate some help and perhaps example of how this could or has been done.

I have come across examples where the line of code below in the ModelBinder

var typeValue = bindingContext.ValueProvider.GetValue("ModelType");

is supposed to read the value. However, it does not work in my model binder and typeValue is always something like below

typeValue
{}
Culture: {}
FirstValue: null
Length: 0
Values: {}
Results View: Expanding the Results View will enumerate the IEnumerable

I have also noticed that

bindingContext.ValueProvider
Count = 2
[0]: {Microsoft.AspNetCore.Mvc.ModelBinding.RouteValueProvider}
[1]: {Microsoft.AspNetCore.Mvc.ModelBinding.QueryStringValueProvider}

Which probably means that as it is I do not stand a chance to read anything from the body.

Do I perhaps need a "formatter" in the mix in order to get desired result?

Does a reference implementation for a similar custom model binder already exist somewhere so that I can simply use it, perhaps with some simple mods?

Thank you.

0 Answers0