I'm trying to create a very simple model binder for ObjectId types in my models but can't seem to make it work so far.
Here's the model binder:
public class ObjectIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var result = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
return Task.FromResult(new ObjectId(result.FirstValue));
}
}
This is the ModelBinderProvider I've coded:
public class ObjectIdModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(ObjectId))
{
return new BinderTypeModelBinder(typeof(ObjectIdModelBinder));
}
return null;
}
}
Here's the class I'm trying to bind the body parameter to:
public class Player
{
[BsonId]
[ModelBinder(BinderType = typeof(ObjectIdModelBinder))]
public ObjectId Id { get; set; }
public Guid PlatformId { get; set; }
public string Name { get; set; }
public int Score { get; set; }
public int Level { get; set; }
}
This is the action method:
[HttpPost("join")]
public async Task<SomeThing> Join(Player player)
{
return await _someService.DoSomethingOnthePlayer(player);
}
For this code to work, I mean for the model binder to run, I inherited the controller from Controller and removed the [FromBody]
attribute from the Player parameter.
When I run this, I can step into BindModelAsync method of the model binder, however I can't seem to get the Id parameter value from the post data. I can see the bindingContext.FieldName is correct; it is set to Id but result.FirstValue is null.
I've been away from Asp.Net MVC for a while, and it seems lots of things have been changed and became more confusing :-)
EDIT Based on comments I think I should provide more context.
If I put [FromBody] before the Player action parameter, player is set to null. If I remove [FromBody], player is set to a default value, not to the values I post. The post body is shown below, it's just a simple JSON:
{
"Id": "507f1f77bcf86cd799439011"
"PlatformId": "9c8aae0f-6aad-45df-a5cf-4ca8f729b70f"
}
If I remove [FromBody], player is set to a default value, not to the values I post.
Reading data from the body is opt-in (unless you're using [ApiController]
). When you remove [FromBody]
from your Player
parameter, the model-binding process will look to populate properties of Player
using the route, query-string and form-values, by default. In your example, there are no such properties in these locations and so none of Player
's properties get set.
If I put [FromBody] before the Player action parameter, player is set to null.
With the presence of the [FromBody]
attribute, the model-binding process attempts to read from the body according to the Content-Type
provided with the request. If this is application/json
, the body will be parsed as JSON and mapped to your Player
's properties. In your example, the JSON-parsing process fails as it doesn't know how to convert from a string
to an ObjectId
. When this happens, ModelState.IsValid
within your controller will return false
and your Player
parameter will be null
.
For this code to work, I mean for the model binder to run, I inherited the controller from Controller and removed the [FromBody] attribute from the Player parameter.
When you remove [FromBody]
, the [ModelBinder(...)]
attribute you've set on your Id
property is respected and so your code runs. However, with the presence of [FromBody
], this attribute effectively is ignored. There's a lot going on behind-the-scenes here, but essentially it boils down to the fact that you've already opted-in to model-binding from the body as JSON and that's where model-binding stops in this scenario.
I mentioned above that it's the JSON-parsing process that's failing here due to not understanding how to process ObjectId
. As this JSON-parsing is handled by Newtonsoft.Json (aka JSON.NET), a possible solution is to create a custom JsonConverter
. This is covered well here on Stack Overflow, so I won't go into the details of how it works. Here's a complete example (error-handling omitted for brevity and laziness):
public class ObjectIdJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>
objectType == typeof(ObjectId);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
ObjectId.Parse(reader.Value as string);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
writer.WriteValue(((ObjectId)value).ToString());
}
To make use of this, just replace your existing [ModelBinder(...)]
attribute with a [JsonConverter(...)]
attribute, like this:
[BsonId]
[JsonConverter(typeof(ObjectIdJsonConverter))]
public ObjectId Id { get; set; }
Alternatively, you can register ObjectIdJsonConverter
globally so that it applies to all ObjectId
properties, using something like this in Startup.ConfigureServices
:
services.AddMvc()
.AddJsonOptions(options =>
options.SerializerSettings.Converters.Add(new ObjectIdJsonConverter());
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With