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.
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:
bindingContext.Model = new User() { Id = 2, Name = "foo" };
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.
Yes, I think you should be able tor read the body as stringasync() or with a valueprovider instead easily as below.
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("Id").AttemptedValue, out id);
bindingContext.Model = new User() { Id = id, Name = "Foo" };
return true;
}
}
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.
I noticed you said you think the following should work but I've found it doesn't and the string comes back empty:
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!) :-)
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?
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.
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?
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.
Formatter approach you have used was probably a better approach to ensure to read it only once and as you wanted.
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?
This is to support version changes and maintain backwards compatibility. We've abstracted our representations from our resources on a per version basis (where the version is specified in the Accept or Content-Type header).
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, a specialised ActionResult would perform the transform.
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 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.
Using the MediaTypeFormatter approach gives a nice symmetry of transforming incoming and outgoing values that I believe will reduce mistakes vs. two different approaches for incoming and outgoing (incoming using value provider dictionary grab/cast and outgoing
using either custom action invoker or custom media type formatter).
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. Perhaps if there were collections of MediaTypeFormatters per controller it would make this more efficient.
Does this answer your question?
BTW You say "Model binding already reads the body by default for you", 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.
LittleClive
Member
91 Points
65 Posts
Re: How do I add a custom ModelBinder?
Feb 20, 2012 07:40 PM|LINK
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.
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:
bindingContext.Model = new User() { Id = 2, Name = "foo" };
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.
Thanks
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 20, 2012 08:24 PM|LINK
Yes, I think you should be able tor read the body as stringasync() or with a valueprovider instead easily as below.
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("Id").AttemptedValue, out id); bindingContext.Model = new User() { Id = id, Name = "Foo" }; return true; } }LittleClive
Member
91 Points
65 Posts
Re: How do I add a custom ModelBinder?
Feb 20, 2012 09:30 PM|LINK
Thanks for replying again.
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.
I noticed you said you think the following should work but I've found it doesn't and the string comes back empty:
Console.WriteLine(actionContext.Request.Content.ReadAsStringAsync().Result);
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!) :-)
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 20, 2012 09:34 PM|LINK
Yes, 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.
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 20, 2012 10:06 PM|LINK
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?
LittleClive
Member
91 Points
65 Posts
Re: How do I add a custom ModelBinder?
Feb 21, 2012 07:32 AM|LINK
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.
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?
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 21, 2012 05:49 PM|LINK
Yes, I will open a bug to track this issue internally in our team. Please feel free to open a connect bug too.
Thanks.
dravva
Member
142 Points
31 Posts
Microsoft
Re: How do I add a custom ModelBinder?
Feb 21, 2012 09:28 PM|LINK
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.
Formatter approach you have used was probably a better approach to ensure to read it only once and as you wanted.
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?
Thanks.
LittleClive
Member
91 Points
65 Posts
Re: How do I add a custom ModelBinder?
Feb 22, 2012 09:13 AM|LINK
This is to support version changes and maintain backwards compatibility. We've abstracted our representations from our resources on a per version basis (where the version is specified in the Accept or Content-Type header).
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, a specialised ActionResult would perform the transform.
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 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.
Using the MediaTypeFormatter approach gives a nice symmetry of transforming incoming and outgoing values that I believe will reduce mistakes vs. two different approaches for incoming and outgoing (incoming using value provider dictionary grab/cast and outgoing using either custom action invoker or custom media type formatter).
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. Perhaps if there were collections of MediaTypeFormatters per controller it would make this more efficient.
Does this answer your question?
BTW You say "Model binding already reads the body by default for you", 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.