Please can someone let me know how to add a custom model binder? I'm after some way of transforming different input representations to a common type (similar to the string->Geopoint) demo in the preview. Previously I was using an HttpOperationHandler.
For example, MVC has the following but that takes an System.Web.Mvc.IModelBinder not an System.Web.Http.ModelBinding.IModelBinder:
System.Web.Mvc.ModelBinders.Binders.Add(xxx);
What I've tried
I've seen I could potentially use a custom DependencyResolver to provide a custom ModelBinderProvider, from that I could then return a custom model binder. However I'm sure there must be a simpler way. Additionally, what implementation of IModelBinder should
I be deriving from in order to get default behaviour?
Also if I try and read the body of the content in my custom ModelBinder it comes out blank (the headers are available and look ok but doing say actionContext.Request.Content.ReadAsStringAsync().Result is empty - perhaps something has already read the content?
I've noticed actionContext.RequestContentKeyValueModel already seems to know what's in the content so perhaps I'm too late:
// The following all return empty strings despite actionContext.RequestContentKeyValueModel containing the values
var contentFromRequest = actionContext.Request.Content.ReadAsStringAsync().Result;
var contentFromControllerContext = actionContext.Request.Content.ReadAsStringAsync().Result;
// Clearly not the way to do it, but gives empty string too
var contentFromInputStream =
newStreamReader((actionContext.ControllerContext.Request.Properties["MS_HttpContext"]
as System.Web.HttpContextWrapper).Request.InputStream).ReadToEnd();
There is also a CustomModelBinderAttribute that I could derive from and apply my parameters or service(?) but it never seems to get triggered (but I'm not sure what I should be applying it to).
I've also looked into creating a custom HttpActionDescriptor mostly to transform the return value. This adapts the default ActionDescriptor and overrides the execute so that I can transform the return value which seems to work. I don't think I can do anything
for the request in here though. I've created a custom ApiControllerActionInvoker in order to wrap the actionContext.ActionDescriptor property.
Additional objectives
I would like my model binder to be able to switch based on values in the request header in order to support versioning. e.g. if v1 in header and destination type is BookModel then use v1BookTransform otherwise use v2BookTransform.
Keep as much out of the box hook-points and functionality as possible (i.e. I'd rather replace a small bit rather than end up plugging in a replacement that means we need to re-implement a load of lost functionality like executing MediaTypeFormatters).
I haven't tried adding custom model binders, but you can add MediaTypeFormatters on the GlobalConfiguration property of the MvcApplication (Global.asax). MediaTypeFormatters are the more Web API way to do this. I believe model binding in Web API uses the
formatters rather than the MVC model binders, but I may be wrong. I know that at least filters can work across both MVC and Web API.
MediaTypeFormatters are the more Web API way to do this.
It seems to me that MediaTypeFormatters cover a different need. I would also like to know how to create a custom model binder because I was also using HttpOperationHandlers in WCF Web API to fill out some of my input parameters based on data in the headers.
So far I haven't had any luck.
Interesting to see you say that, i was reading this and going the other direction thinking that the use of a MediaTypeFormatter was kinda blurred with ModelBinder..at least on the processing of input side
It seems to me that MediaTypeFormatters cover a different need. I would also like to know how to create a custom model binder because I was also using HttpOperationHandlers in WCF Web API to fill out some of my input parameters based on data in the headers.
So far I haven't had any luck.
MediaTypeFormatter evolved as a specialization of the HttpOperationHandler for formatting. I'll let Brad or someone else answer, though, as I'm only aware of MediaTypeFormatter and FormatterSelector (for conneg) as the primary mechanisms for what it sounds
like you are doing. Model binding in Web API, by default does simple stuff like taking query string or form-urlencoded parameters. (At least, I think it picks up form parameters.)
Based on that it almost sounds like the MediaTypeFormatter should just be a thing that can provide ValueProviders and write to the response stream
That might be possible except for the fact that you can also self-host Web API, and Conneg is available via FormatterSelector directly. You may be on to something as to how they actually implement the integration, though.
If you're interested I had a long discussion about this on the preview forums (http://wcf.codeplex.com/discussions/297342 - obviously don't post here :-)). I don't believe MediaTypeFormatters are going
to solve my issue, with the previous HttpOperationHandler it was possible to hook them all up once during start-up to specific operations and then have them deal with checking the version being requested and handling the transformation without having to do
a big lookup of which resource is being transformed (since that had already been done during startup to attach the correct handler to the correct operation).
MediaType formatters on the other hand operate on all messages, so I'd need some big lookup in there to map to a transformer, also it hasn't deserialized at this point so I'd be doing that too. All of which could be avoided (or it least done once during
startup) with the operation handlers (and even they had some drawbacks particularly with the input messages, on output I could chain together handlers optionally perform a transform but on the way in, this was not possible so I did need a slight switch but
it was only switching on version not on message type).
Be great to see Transformation and Versioning being talked about a bit more. If we're taking the wrong approach then I'd rather know now :-)
At the moment even a basic working example of a custom Model Binder would be helpful at this point.
Edit: There does seem to be a new CanReadType/CanWriteType on MediaTypeFormatters which potentially helps but there is no context passed in (e.g. request headers) so this doesn't solve my issue but I can see that if MTFs were enhanced in
a future release, these could be the way to go rather than model binders.
Edit 2: Actually, it looks like MediaTypeFormatters will work for me, the "Type" argument passed into CanRead is the type the controller is expecting so I can opt into that regardless of header since that is version agnostic. Then during
the OnReadFromStreamAsync I just check the contentHeaders.ContentType.Parameters collection for the version and switch the type to deserialize. The deserialized value is then run through a transformer and returned as the type the service expects and it all
seems to work. It doesn't really answer the original question but this has solved my issue
Thanks to panesofglass for suggesting - I don't think I'd have wanted to do this using Preview 6, but the CanReadType(Type type) certainly makes this appear more efficient and keeps the type lookup out of my code. I just hope Web API can handle hundreds
of mediaTypeFormatters being added though
I'm not sure if this will be of interest to you, but Glenn and I worked on a means of extracting the conneg mechanism. You can then use whatever style formatter you may want. You can get the source on
github or pull the package from
nuget.
Please see this simple scenario of implementing and configuring a custom modelbinder.
It may not solve all your issues here but should point you to the right direction though.
public class CustomModelBinderProvider : ModelBinderProvider
{
CustomModelBinder cmb = new CustomModelBinder();
public CustomModelBinderProvider()
{
//Console.WriteLine("In CustomModelBinderProvider ctr");
}
public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(User))
{
return cmb;
}
return null;
}
}
public class CustomModelBinder : IModelBinder
{
public CustomModelBinder()
{
//Console.WriteLine("In CustomModelBinder ctr");
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
//Console.WriteLine("In BindModel");
bindingContext.Model = new User() { Id = 2, Name = "foo" };
return true;
}
}
Configuring the custom ModelBinder for the whole service
IEnumerable<object> modelBinderProviderServices = config.ServiceResolver.GetServices(typeof(ModelBinderProvider));
List<Object> services = new List<object>(modelBinderProviderServices);
services.Add(new CustomModelBinderProvider());
config.ServiceResolver.SetServices(typeof(ModelBinderProvider), services.ToArray());
Configuring for specific action method
public HttpResponseMessage<Contact> Get([ModelBinder(typeof(CustomModelBinderProvider))] User user)
LittleClive
Member
91 Points
65 Posts
How do I add a custom ModelBinder?
Feb 17, 2012 08:35 PM|LINK
Please can someone let me know how to add a custom model binder? I'm after some way of transforming different input representations to a common type (similar to the string->Geopoint) demo in the preview. Previously I was using an HttpOperationHandler.
For example, MVC has the following but that takes an System.Web.Mvc.IModelBinder not an System.Web.Http.ModelBinding.IModelBinder:
System.Web.Mvc.ModelBinders.Binders.Add(xxx);
What I've tried
I've seen I could potentially use a custom DependencyResolver to provide a custom ModelBinderProvider, from that I could then return a custom model binder. However I'm sure there must be a simpler way. Additionally, what implementation of IModelBinder should I be deriving from in order to get default behaviour?
Also if I try and read the body of the content in my custom ModelBinder it comes out blank (the headers are available and look ok but doing say actionContext.Request.Content.ReadAsStringAsync().Result is empty - perhaps something has already read the content? I've noticed actionContext.RequestContentKeyValueModel already seems to know what's in the content so perhaps I'm too late:
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
// The following all return empty strings despite actionContext.RequestContentKeyValueModel containing the values
var contentFromRequest = actionContext.Request.Content.ReadAsStringAsync().Result;
var contentFromControllerContext = actionContext.Request.Content.ReadAsStringAsync().Result;
// Clearly not the way to do it, but gives empty string too
var contentFromInputStream = new StreamReader((actionContext.ControllerContext.Request.Properties["MS_HttpContext"] as System.Web.HttpContextWrapper).Request.InputStream).ReadToEnd();
There is also a CustomModelBinderAttribute that I could derive from and apply my parameters or service(?) but it never seems to get triggered (but I'm not sure what I should be applying it to).
I've also looked into creating a custom HttpActionDescriptor mostly to transform the return value. This adapts the default ActionDescriptor and overrides the execute so that I can transform the return value which seems to work. I don't think I can do anything for the request in here though. I've created a custom ApiControllerActionInvoker in order to wrap the actionContext.ActionDescriptor property.
Additional objectives
Happy to share code if needed.
Thanks
panesofglass
Member
730 Points
237 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 08:53 PM|LINK
I haven't tried adding custom model binders, but you can add MediaTypeFormatters on the GlobalConfiguration property of the MvcApplication (Global.asax). MediaTypeFormatters are the more Web API way to do this. I believe model binding in Web API uses the formatters rather than the MVC model binders, but I may be wrong. I know that at least filters can work across both MVC and Web API.
dcstraw
Member
37 Points
21 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:00 PM|LINK
It seems to me that MediaTypeFormatters cover a different need. I would also like to know how to create a custom model binder because I was also using HttpOperationHandlers in WCF Web API to fill out some of my input parameters based on data in the headers. So far I haven't had any luck.
chrisortman
Member
38 Points
23 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:06 PM|LINK
Interesting to see you say that, i was reading this and going the other direction thinking that the use of a MediaTypeFormatter was kinda blurred with ModelBinder..at least on the processing of input side
panesofglass
Member
730 Points
237 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:07 PM|LINK
MediaTypeFormatter evolved as a specialization of the HttpOperationHandler for formatting. I'll let Brad or someone else answer, though, as I'm only aware of MediaTypeFormatter and FormatterSelector (for conneg) as the primary mechanisms for what it sounds like you are doing. Model binding in Web API, by default does simple stuff like taking query string or form-urlencoded parameters. (At least, I think it picks up form parameters.)
chrisortman
Member
38 Points
23 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:20 PM|LINK
Based on that it almost sounds like the MediaTypeFormatter should just be a thing that can provide ValueProviders and write to the response stream
panesofglass
Member
730 Points
237 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:22 PM|LINK
That might be possible except for the fact that you can also self-host Web API, and Conneg is available via FormatterSelector directly. You may be on to something as to how they actually implement the integration, though.
LittleClive
Member
91 Points
65 Posts
Re: How do I add a custom ModelBinder?
Feb 17, 2012 09:58 PM|LINK
If you're interested I had a long discussion about this on the preview forums (http://wcf.codeplex.com/discussions/297342 - obviously don't post here :-)). I don't believe MediaTypeFormatters are going to solve my issue, with the previous HttpOperationHandler it was possible to hook them all up once during start-up to specific operations and then have them deal with checking the version being requested and handling the transformation without having to do a big lookup of which resource is being transformed (since that had already been done during startup to attach the correct handler to the correct operation).
MediaType formatters on the other hand operate on all messages, so I'd need some big lookup in there to map to a transformer, also it hasn't deserialized at this point so I'd be doing that too. All of which could be avoided (or it least done once during startup) with the operation handlers (and even they had some drawbacks particularly with the input messages, on output I could chain together handlers optionally perform a transform but on the way in, this was not possible so I did need a slight switch but it was only switching on version not on message type).
Be great to see Transformation and Versioning being talked about a bit more. If we're taking the wrong approach then I'd rather know now :-)
At the moment even a basic working example of a custom Model Binder would be helpful at this point.
Edit: There does seem to be a new CanReadType/CanWriteType on MediaTypeFormatters which potentially helps but there is no context passed in (e.g. request headers) so this doesn't solve my issue but I can see that if MTFs were enhanced in a future release, these could be the way to go rather than model binders.
Edit 2: Actually, it looks like MediaTypeFormatters will work for me, the "Type" argument passed into CanRead is the type the controller is expecting so I can opt into that regardless of header since that is version agnostic. Then during the OnReadFromStreamAsync I just check the contentHeaders.ContentType.Parameters collection for the version and switch the type to deserialize. The deserialized value is then run through a transformer and returned as the type the service expects and it all seems to work. It doesn't really answer the original question but this has solved my issue
Thanks to panesofglass for suggesting - I don't think I'd have wanted to do this using Preview 6, but the CanReadType(Type type) certainly makes this appear more efficient and keeps the type lookup out of my code. I just hope Web API can handle hundreds of mediaTypeFormatters being added though
panesofglass
Member
730 Points
237 Posts
Re: How do I add a custom ModelBinder?
Feb 20, 2012 05:21 PM|LINK
I'm not sure if this will be of interest to you, but Glenn and I worked on a means of extracting the conneg mechanism. You can then use whatever style formatter you may want. You can get the source on github or pull the package from nuget.
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 20, 2012 06:08 PM|LINK
Please see this simple scenario of implementing and configuring a custom modelbinder.
It may not solve all your issues here but should point you to the right direction though.
public class CustomModelBinderProvider : ModelBinderProvider { CustomModelBinder cmb = new CustomModelBinder(); public CustomModelBinderProvider() { //Console.WriteLine("In CustomModelBinderProvider ctr"); } public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType == typeof(User)) { return cmb; } return null; } } public class CustomModelBinder : IModelBinder { public CustomModelBinder() { //Console.WriteLine("In CustomModelBinder ctr"); } public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { //Console.WriteLine("In BindModel"); bindingContext.Model = new User() { Id = 2, Name = "foo" }; return true; } } Configuring the custom ModelBinder for the whole service IEnumerable<object> modelBinderProviderServices = config.ServiceResolver.GetServices(typeof(ModelBinderProvider)); List<Object> services = new List<object>(modelBinderProviderServices); services.Add(new CustomModelBinderProvider()); config.ServiceResolver.SetServices(typeof(ModelBinderProvider), services.ToArray()); Configuring for specific action method public HttpResponseMessage<Contact> Get([ModelBinder(typeof(CustomModelBinderProvider))] User user)