Ahmad_Hamdan
Ahmad_Hamdan

Reputation: 87

Custom Model Binder In Asp.Net Core for Sub Classes

I have a scenario where I have a certain base class we will call it "PagingCriteriaBase"

public  class PagingCriteriaBase : CriteriaBase
{
    public Int32 CountOfItemsPerPage { get; set; }
    public SortOrder SortingOrder { get; set; }
    public String SortBy { get; set; }
    public  Int32 PageNo { get; set; }
    public PagingCriteriaBase(Int32 pageNo,Int32 countOfItemsPerPage, SortOrder sortingOrder, String sortBy,Int32 draw)
    {
        this.PageNo = pageNo>0?pageNo:1;
        this.CountOfItemsPerPage = countOfItemsPerPage>0?countOfItemsPerPage:10;
        this.SortBy = sortBy;
        this.SortingOrder = sortingOrder;
        this.Draw = draw;
    }
}

and then I have other classes that will inherit from "PagingCriteriaBase", for example

public class UserCriteria:PagingCriteriaBase
{
    public String Email { get; set; }
    public String DisplayName { get; set; }

    public UserCriteria():base(1,0,SortOrder.Asc,"",1)
    {

    }
    public UserCriteria(Int32 pageNo,Int32 countOfItemsPerPage, SortOrder sortingOrder, String sortBy, Int32 draw)
        :base(pageNo, countOfItemsPerPage,sortingOrder,sortBy,draw)
    {
    }
}

Now what I would like to do is that I wanted to create a Model Binder that will be used with Web API methods, and the model binder will be used with all of the subclasses of "PagingCriteriaBase", the purpose of this model binder is to set some properties according to data coming from ajax requests, I tried to do the following:

  1. I created a class that implements "IModelBinder" as follows:

    public class PagingModelBinder : IModelBinder
    {
    
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (!bindingContext.ModelType.IsSubclassOf(typeof(PagingCriteriaBase)))
        {
            return Task.FromResult(false);
        }
    
        String startModelName = "start";
        String lengthModelName = "length";
        var startResult = bindingContext.ValueProvider.GetValue(startModelName);
        var lengthResult = bindingContext.ValueProvider.GetValue(lengthModelName);
        Int32 start, length;
        if (!Int32.TryParse(startResult.FirstValue, out start))
        {
            start = 0;
        }
        if (!Int32.TryParse(lengthResult.FirstValue, out length))
        {
            length = SystemProp.PAGE_SIZE;
        }
        else
        {
            length = 20;
        }
        var model = Activator.CreateInstance(bindingContext.ModelType);
    
        Int32 pageNo = (int)Math.Ceiling((decimal)start / length);
    
        bindingContext.ModelState.SetModelValue("PageNo", new ValueProviderResult(pageNo.ToString()));
        bindingContext.ModelState.SetModelValue("CountOfItemsPerPage", new ValueProviderResult(length.ToString()));
        bindingContext.Model = model;
        var mProv = (IModelMetadataProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
    
        bindingContext.Result = ModelBindingResult.Success(model);
    
        return Task.CompletedTask;
    }
    }
    
  2. I created a ModelBinderProvider as follows:

    public class PagingEntityBinderProvider:IModelBinderProvider
    {
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
    
        if (context.Metadata.ModelType == typeof(PagingCriteriaBase))
        {
            return new BinderTypeModelBinder(typeof(PagingModelBinder));
        }
    
        return null;
    }
    }
    
  3. I registered the model binder using:

    services.AddMvc(op => op.ModelBinderProviders.Insert(0, new PagingEntityBinderProvider())) ;
    
  4. In my Web API method I did the following:

     public IActionResult GetAll([ModelBinder(typeof(PagingModelBinder))]UserCriteria crit)
    {
     //Code goes here
    }
    

When I used the model binder as above I found out that once the code reaches the Web API Methods, nothing from the values in the class is changed, for example "PageNo" property stays 1, So what I need to do is to have the model binder set all the related property for the subclass object regardless of the type of the class itself and in the end once the code reaches the Web API method, the model will have all properties set correctly, can you please point me to what do I need to change in my code to handle this?

Please note that I am using Asp.Net Core 2.0

Upvotes: 1

Views: 2416

Answers (1)

Laksmono
Laksmono

Reputation: 690

I guess that's because you haven't set any property of the model, only instantiated it.

I think we can go trough all the properties of the subclass with reflection and set the value based on the model state value (assuming that the property name is the same with the model state key)

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ...
        bindingContext.ModelState.SetModelValue("PageNo", new ValueProviderResult(pageNo.ToString()));
        bindingContext.ModelState.SetModelValue("CountOfItemsPerPage", new ValueProviderResult(length.ToString()));

        ModelStateEntry v;
        foreach (PropertyInfo pi in bindingContext.ModelType.GetProperties())
        {
            if (bindingContext.ModelState.TryGetValue(pi.Name, out v))
            {
                try
                {
                    pi.SetValue(model, v.RawValue);
                }
                catch
                {
                }
            }
        }

        bindingContext.Model = model;
        ...
    }

And change your PagingEntityBinderProvider

public class PagingEntityBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (typeof(PagingCriteriaBase).IsAssignableFrom(context.Metadata.ModelType))
        {
            return new BinderTypeModelBinder(typeof(PagingModelBinder));
        }

        return null;
    }
}

And remove the ModelBinder Attribute from the Web API Method

public IActionResult GetAll(UserCriteria crit)
{
    //Code goes here
}

Upvotes: 1

Related Questions