How do I add a custom ModelBinder?http://forums.asp.net/t/1770906.aspx/1?How+do+I+add+a+custom+ModelBinder+Wed, 22 Feb 2012 09:13:25 -050017709064838221http://forums.asp.net/p/1770906/4838221.aspx/1?How+do+I+add+a+custom+ModelBinder+How do I add a custom ModelBinder? <p>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-&gt;Geopoint) demo in the preview. Previously I was using an HttpOperationHandler.</p> <p>For example, MVC has the following but that takes an System.Web.Mvc.IModelBinder not an System.Web.Http.ModelBinding.IModelBinder:</p> <p><em>System.Web.Mvc.ModelBinders.Binders.Add(xxx);</em></p> <h2>What I've tried</h2> <p>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?</p> <p>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:</p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas; color:blue">public</span><span style="font-size:8.0pt; font-family:Consolas"> <span style="color:blue">bool</span> BindModel(System.Web.Http.Controllers.<span style="color:#2b91af">HttpActionContext</span> actionContext, <span style="color:#2b91af">ModelBindingContext</span> bindingContext)</span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas">{</span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas; color:green">&nbsp; // The following all return empty strings despite actionContext.RequestContentKeyValueModel containing the values&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas; color:blue">&nbsp; var</span><span style="font-size:8.0pt; font-family:Consolas"> contentFromRequest = actionContext.Request.Content.ReadAsStringAsync().Result;</span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas; color:blue">&nbsp; var</span><span style="font-size:8.0pt; font-family:Consolas"> contentFromControllerContext = actionContext.Request.Content.ReadAsStringAsync().Result;</span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas">&nbsp;&nbsp;<span style="color:green">// Clearly not the way to do it, but gives empty string too</span></span></p> <p class="MsoNormal" style="margin-bottom:.0001pt; line-height:normal; text-autospace:none"> <span style="font-size:8.0pt; font-family:Consolas">&nbsp; <span style="color:blue">var</span> contentFromInputStream = <span style="color:blue">new</span> <span style="color:#2b91af">StreamReader</span>((actionContext.ControllerContext.Request.Properties[<span style="color:#a31515">&quot;MS_HttpContext&quot;</span>] <span style="color:blue">as</span> System.Web.<span style="color:#2b91af">HttpContextWrapper</span>).Request.InputStream).ReadToEnd();</span></p> <p>&nbsp;</p> <p>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).</p> <p>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.</p> <h2>Additional objectives</h2> <ul> <li>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. </li><li>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). </li></ul> <p>Happy to share code if needed.</p> <p>Thanks</p> 2012-02-17T20:35:32-05:004838240http://forums.asp.net/p/1770906/4838240.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>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.</p> 2012-02-17T20:53:47-05:004838244http://forums.asp.net/p/1770906/4838244.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p></p> <blockquote><span class="icon-blockquote"></span> <h4>panesofglass</h4> MediaTypeFormatters are the more Web API way to do this.</blockquote> <p></p> <p>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.</p> 2012-02-17T21:00:56-05:004838250http://forums.asp.net/p/1770906/4838250.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>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</p> <p></p> 2012-02-17T21:06:08-05:004838251http://forums.asp.net/p/1770906/4838251.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p></p> <blockquote><span class="icon-blockquote"></span> <h4>dcstraw</h4> <p></p> <p>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.</p> <p></p> </blockquote> <p></p> <p>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.)</p> 2012-02-17T21:07:07-05:004838261http://forums.asp.net/p/1770906/4838261.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Based on that it almost sounds like the MediaTypeFormatter should just be a thing that can provide ValueProviders and write to the response stream</p> 2012-02-17T21:20:03-05:004838263http://forums.asp.net/p/1770906/4838263.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p></p> <blockquote><span class="icon-blockquote"></span> <h4>chrisortman</h4> <p></p> <p>Based on that it almost sounds like the MediaTypeFormatter should just be a thing that can provide ValueProviders and write to the response stream</p> <p></p> </blockquote> <p></p> <p>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.</p> 2012-02-17T21:22:21-05:004838277http://forums.asp.net/p/1770906/4838277.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>If you're interested I had a long discussion about this on the preview forums (<a href="http://wcf.codeplex.com/discussions/297342">http://wcf.codeplex.com/discussions/297342</a>&nbsp;- 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).</p> <p>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&nbsp;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).</p> <p>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 :-)</p> <p>At the moment even a basic working example of a custom Model Binder would be helpful at this point.</p> <p><strong>Edit:</strong> There does seem to be a new CanReadType/CanWriteType on MediaTypeFormatters which potentially&nbsp;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.</p> <p><strong>Edit 2:</strong> Actually, it looks like MediaTypeFormatters will work for me, the &quot;Type&quot; 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 <img title="Laughing" border="0" alt="Laughing" src="http://forums.asp.net/scripts/tiny_mce/plugins/emotions/img/smiley-laughing.gif"></p> <p>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&nbsp;just hope&nbsp;Web API&nbsp;can handle&nbsp;hundreds of mediaTypeFormatters being added though <img title="Undecided" border="0" alt="Undecided" src="http://forums.asp.net/scripts/tiny_mce/plugins/emotions/img/smiley-undecided.gif"></p> <p>&nbsp;</p> 2012-02-17T21:58:43-05:004841654http://forums.asp.net/p/1770906/4841654.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>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 <a href="https://github.com/panesofglass/WebApi.Conneg">github</a> or pull the package from <a href="https://nuget.org/packages/AspNetWebApi.Conneg">nuget</a>.</p> 2012-02-20T17:21:34-05:004841690http://forums.asp.net/p/1770906/4841690.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Please see this simple scenario of implementing and configuring a custom modelbinder.</p> <p>It may not solve all your issues here but should point you to the right direction though.</p> <pre class="prettyprint">public class CustomModelBinderProvider : ModelBinderProvider { CustomModelBinder cmb = new CustomModelBinder(); public CustomModelBinderProvider() { //Console.WriteLine(&quot;In CustomModelBinderProvider ctr&quot;); } 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(&quot;In CustomModelBinder ctr&quot;); } public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { //Console.WriteLine(&quot;In BindModel&quot;); bindingContext.Model = new User() { Id = 2, Name = &quot;foo&quot; }; return true; } } Configuring the custom ModelBinder for the whole service IEnumerable&lt;object&gt; modelBinderProviderServices = config.ServiceResolver.GetServices(typeof(ModelBinderProvider)); List&lt;Object&gt; services = new List&lt;object&gt;(modelBinderProviderServices); services.Add(new CustomModelBinderProvider()); config.ServiceResolver.SetServices(typeof(ModelBinderProvider), services.ToArray()); Configuring for specific action method public HttpResponseMessage&lt;Contact&gt; Get([ModelBinder(typeof(CustomModelBinderProvider))] User user)</pre> 2012-02-20T18:08:48-05:004841786http://forums.asp.net/p/1770906/4841786.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>That's great and pretty close to what I have , it's good to see confirmation that the DependencyResolver is the way to inject the CMB via a ModelBinderProvider.</p> <p>One thing that your example doesn't have and something I didn't/couldn't get working is the ability to read the incoming message. So rather than:</p> <p><em>bindingContext.Model = new User() { Id = 2, Name = &quot;foo&quot; };</em></p> <p>Please could you show me how you'd read the request body because I'm finding it comes out blank using the lines of code in my example.</p> <p>Thanks</p> 2012-02-20T19:40:33-05:004841836http://forums.asp.net/p/1770906/4841836.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Yes, I think you should be able tor read the body as stringasync() or with a valueprovider instead easily as below.</p> <pre class="prettyprint">public class CustomModelBinder : IModelBinder { public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { //Reading the whole body Console.WriteLine(actionContext.Request.Content.ReadAsStringAsync().Result); //Reading specific values with ValueProvider int id; Int32.TryParse(bindingContext.ValueProvider.GetValue(&quot;Id&quot;).AttemptedValue, out id); bindingContext.Model = new User() { Id = id, Name = &quot;Foo&quot; }; return true; } }</pre> 2012-02-20T20:24:23-05:004841881http://forums.asp.net/p/1770906/4841881.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Thanks for replying again.</p> <p>Yes the ValueProvider works, it's using the JsonKeyValueModel but for what I need it's a bit of an inconvenient way of getting the values - I'm not sure how it deals with hierarchical values yet.</p> <p>I noticed you said you <em>think</em> the following should work but I've found it doesn't and the string comes back empty:</p> <p>Console.WriteLine(actionContext.Request.Content.ReadAsStringAsync().Result);</p> <p>Have you tried it within the context of an MVC Web API project (i.e. not self-hosted)? If you say it should work then I'll try and post a trimmed down a code example to justify my lofty claim (and most likely highlight what I'm doing wrong!)&nbsp;:-)</p> 2012-02-20T21:30:00-05:004841886http://forums.asp.net/p/1770906/4841886.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Yes,&nbsp;I saw it working in a self hosted project but not web hosted. But I would have thought this should have not much affect how it is hosted.</p> <p>&nbsp;</p> <p>&nbsp;</p> 2012-02-20T21:34:58-05:004841901http://forums.asp.net/p/1770906/4841901.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Yes, I see that it does not work in a webhosted for some reason. Could you consider getting the values instead from the value providers?</p> 2012-02-20T22:06:02-05:004842414http://forums.asp.net/p/1770906/4842414.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p></p> <blockquote><span class="icon-blockquote"></span> <h4>dravva</h4> <p></p> <p>Yes, I see that it does not work in a webhosted for some reason. Could you consider getting the values instead from the value providers?</p> <p></p> </blockquote> <p></p> <p>Glad it wasn't me going crazy! It seems like MediaTypeFormatters are the way to go for me since they have the CanReadType, CanWriteType filters. I'm not sure if it's recommended to have this many MediaTypeFormatters though - it doesn't seem like this is what they were intended for but it does seem to work.</p> <p>It seems like this is a bug then if the behaviour of webhost differs from self-host for this type of scenario? I assume something in webhost has already read the request stream and therefore it cannot be read again. Is this something I should raise on Connect?</p> 2012-02-21T07:32:20-05:004843641http://forums.asp.net/p/1770906/4843641.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>Yes, I will open a bug to track this issue internally in our team. Please feel free to open a connect bug too.</p> <p>Thanks.</p> 2012-02-21T17:49:05-05:004843886http://forums.asp.net/p/1770906/4843886.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>After thinking more here, it looks like the issue is by design and there is no bug here. The behavior across hosts could be different. It is not expected to read a body multiple times. Model binding already reads the body by default for you and thus you see some inconsistent behavior reading body again across hosts.</p> <p>Formatter approach you have used was probably a better approach to ensure to read it only once and as you wanted.</p> <p>Custom modelbinder with using value provider to read the values was also other right approach but I am not sure why it was not sufficient for your scenarios. Could you tell us more why you could not use this approach?</p> <p>Thanks.</p> 2012-02-21T21:28:21-05:004844788http://forums.asp.net/p/1770906/4844788.aspx/1?Re+How+do+I+add+a+custom+ModelBinder+Re: How do I add a custom ModelBinder? <p>This is to support version changes and maintain backwards compatibility. We've abstracted our representations from our resources&nbsp;on a per version basis (where the version is specified in the Accept or Content-Type header).</p> <p>The controller itself only deals with the latest and greatest internal resource definition. Previously (using MVC3) we'd used model binding to deserialize the incoming message to the appropriate version of the type, then transform it into the internal representation. For outgoing,&nbsp;a specialised&nbsp;ActionResult would perform the transform.</p> <p>Therefore I was trying to map this approach onto Web API, the ValueProvider is not great because plucking values out of a dictionary and casting/converting them&nbsp;isn't as convenient as mapping objects with strongly typed properties. Ideally we'll have a Transformer that takes v2.Book and creates BookDto and vice versa for return values.</p> <p>Using the MediaTypeFormatter approach gives a nice symmetry of transforming incoming and outgoing values that I believe will reduce mistakes&nbsp;vs.&nbsp;two different approaches for incoming and outgoing (incoming using value provider dictionary grab/cast&nbsp;and outgoing using either custom action invoker or custom&nbsp;media type formatter).</p> <p>The downside with MediaTypeFormatters I can see is that I can't easily target (and therefore make more efficient) my media formatter against the specific controller operations it will apply to. I'm hoping that Web API will be efficient enough when looking up the correct mediaTypeFormatter to apply.&nbsp;Perhaps if there were collections of MediaTypeFormatters per controller it would make this more efficient.</p> <p>Does this answer your question?</p> <p>BTW You say &quot;Model binding already reads the body by default for you&quot;, by plugging in a custom model binder I was expecting to override this default behaviour as we did in MVC3 but perhaps the introduction of MediaTypeFormatters means this responsibility is now different. Be good to see an overall diagram of pluggable points with roles and responsibilities.</p> <p>&nbsp;</p> 2012-02-22T09:13:25-05:00