Web Api: custom binding with AutoMapper
By Piotr on (tags: automapper, mvc, Web API, categories: code)In Web Api 2.0 and MVC5 you can only pass: int, bool, double etc.., TimeSpan, DateTime, Guid, decimal, and string values in URI. For any complex type having lists or other complex object within you have to pass serialized JSON/XML in the request body. But what If we want use such complex type from URI? Then read on…
What is binding?
Binding is a mechanism used by the Web Api to mapping request to an object defined in the controller. In this article I will show how to map complex types from URI requests using custom binding and AutoMapper.
I assume you know basics of Web Api bindings and AutoMapper. If not try here (http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api) and here (http://jasona.wordpress.com/2010/02/05/getting-started-with-automapper/).
Sample complex type
public class SampleComplexModel
{
public string SimpleField1 { get; set; }
public SubComplexModel1 ComplexField1 { get; set; }
public SubComplexModel2 ComplexField2 { get; set; }
}
public class SubComplexModel1
{
public string Sub1Field1 { get; set; }
}
public class SubComplexModel2
{
public string Sub2Field1 { get; set; }
}
Without custom binding:
Normally we use it like this:
public class ValuesController : ApiController
{
pubic IEnumerable<string> Get([FromUri] SampleComplexModel model)
...
FromUri attribute forces Web Api to search data in query string, not from request body.
Our sample query string looks like this:
api/Values?SimpleField1=aaa&Sub2Field1=bbb&Sub1Field1=ccc
The result will be: model.SimpleField1 = “aaa”, model.ComplexField1 and model.ComplexField2 will be null – which is absolutely normal. Default behavior is not that smart. If we want to bind those two complex types we need custom binding.
Custom binding
Custom binding allow us to do whatever we like with the request. In this example I will operate only on URI data, but with custom binding we can also get to the body of the request as well.
To map request to our custom object we need to define custom binding by implementing System.Web.Http.ModelBinding.IModelBinder. It only has one method BindModel which return true if binding succeed or false - if not.
Definition of SampleComplexModelBinder
Using our binder is very simple:
public class ValuesController : ApiController
{
public IEnumerable<string> Get([ModelBinder(typeof(SampleComplexModelBinder))]SampleComplexModel model)
...
As you can see there is attribute ModelBinder which takes as parameter type of our binder. Nice and simple! Now, let’s look at our SampleComplexModelBinder:
public class SampleComplexModelBinder : IModelBinder
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
IDictionary<string, string> values = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query))
{
values = actionContext.Request.GetQueryNameValuePairs()
.ToDictionary(pair => pair.Key, pair => pair.Value);
} else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "No input data");
return false;
}
if (bindingContext.ModelType != typeof (SampleComplexModel))
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "SampleComplexModel type expected, not type: " + bindingContext.ModelType.FullName);
return false;
}
var obj = new SampleComplexModel();
try
{
obj.SimpleField1 = values["SimpleField1"];
obj.ComplexField1 = new SubComplexModel1 { Sub1Field1 = values["Sub1Field1"] };
obj.ComplexField2 = new SubComplexModel2 { Sub2Field1 = values["Sub2Field1"] };
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, ex.Message);
return false;
}
bindingContext.Model = obj;
return true;
}
}
The most important lines are:
obj.SimpleField1 = values["SimpleField1"];
obj.ComplexField1 = new SubComplexModel1 { Sub1Field1 = values["Sub1Field1"] };
obj.ComplexField2 = new SubComplexModel2 { Sub2Field1 = values["Sub2Field1"] };
In this block we create total graph of our complex object. But is that mean that we need separate binder to every complex type we have? Well... no ;)
Definition of AutoMapperBinder
Below is very simple binder that use AutoMapper. This solution shows, that we can have only one generic binder to all our complex types. The only thing we should do before is to register our complex types in AutoMapper and define mappings:
public static class AutoMapperConfig
{
public static void RegisterMappings()
{
AutoMapper.Mapper.CreateMap<IDictionary<string, string>, SubComplexModel1>()
.ForMember(dest => dest.Sub1Field1,
opts => opts.MapFrom(src => src["Sub1Field1"]));
AutoMapper.Mapper.CreateMap<IDictionary<string, string>, SubComplexModel2>()
.ForMember(dest => dest.Sub2Field1,
opts => opts.MapFrom(src => src["Sub2Field1"]));
AutoMapper.Mapper.CreateMap<IDictionary<string, string>, SampleComplexModel>()
.ForMember(dest => dest.SimpleField1,
opts => opts.MapFrom(src => src["SimpleField1"]))
.ForMember(dest => dest.ComplexField1,
opts => opts.MapFrom(src => Mapper.Map<IDictionary<string, string>, SubComplexModel1>(src)))
.ForMember(dest => dest.ComplexField2,
opts => opts.MapFrom(src => Mapper.Map<IDictionary<string, string>, SubComplexModel2>(src)));
Note that source type is always IDictionary<string, string>. It is important as this type is used in our AutoMapperBinder. This could be any type that is able to store query string content though.
public class AutoMapperBinder<T> : IModelBinder where T: class
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var values = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query))
{
values = actionContext.Request.GetQueryNameValuePairs()
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "No input data");
return false;
}
T obj = null;
try
{
obj = Mapper.Map<IDictionary<string, string>, T>(values);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, ex.Message);
return false;
}
bindingContext.Model = obj;
return true;
}
}
What changed? Now we only have one line:
obj = Mapper.Map<IDictionary<string, string>, T>(values);
Usage is exactly the same as SampleComplexModelBinder with the difference that we pass also generic type:
public class ValuesController : ApiController
{
public IEnumerable<string> Get([ModelBinder(typeof(AutoMapperBinder<SampleComplexModel>))] SampleComplexModel model)
...
Conclusion
Using AutoMapper with custom bindings may simplified our work a lot. If we have a lot of different complex object we can register them only by AutoMapper. With this approach we can map nested complex object and even list.