2

My API controller has an endpoint that looks like this:

public async Task<IActionResult> Update([FromBody] UpdateCommand command) { /* ... */ }

That command looks like this:

public class UpdateCommand {
  public string Username { get; set; }
  public Id Id { get; set; }               // <--- here's the problem
}

That Id is a value object that sorta looks like this:

public class Id : SimpleValueObject<long> {
  // base class has: IComparable, equality, GetHashCode, etc.
  public Id(long value) : base(value) { }
  public Id(string value) : base(Parse(value)) { }
  private static long Parse(string value) { /* ... */ }
}

The client would send this:

{
  "username": "foo",
  "id": 1
}

Now I want model binding to automagically work. But I'm confused how to do that.

I implemented a IModelBinder and IModelBinderProvider, but that didn't help. Then I noticed the docs say this:

Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.

So I implemented a TypeConverter, and that also didn't help.

Then I thought to implement a JsonConverter<T>, but the framework now uses something other than Newtonsoft, so I didn't get far.

So my question is: what must I do to facilitate automatic binding for my custom type. I only need to know which path to pursue, I'll figure out the rest.

(As a side issue: please help me understand when I should implement a model binder vs type converter vs json converter.)

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
lonix
  • 14,255
  • 23
  • 85
  • 176
  • _"Then I thought to implement a JsonConverter, but the framework now uses something other than Newtonsoft, so I didn't get far."_ Yes, it is `System.Text.Json` and it allows you to write [custom converters](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0) too. – Guru Stron Jul 07 '21 at 08:37
  • @GuruStron Thanks I just discovered that Newtonsoft was replaced by `System.Text.Json` namespace. But please tell me, is it a `JsonConverter` that I should implement, or something else? – lonix Jul 07 '21 at 08:49
  • 1
    It should be `JsonConverter` if you want something to be deserialized to your `Id` type. – Guru Stron Jul 07 '21 at 09:41

2 Answers2

1

I still don't understand when to use a custom model binder vs custom type converter vs custom json converter.

But it seems like the solution for this scenario is a custom JsonConverter<T>.

This works for me:

public class IdConverter : JsonConverter<Id> {

  public override Id? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
    if (reader.TokenType is not JsonTokenType.String and not JsonTokenType.Number)
      throw new JsonException();
    try {
      if (reader.TokenType is JsonTokenType.String) {
        var value = reader.GetString();
        return new Id(value);
      }
      else {
        var value = reader.GetInt64();
        return new Id(value);
      }
    }
    catch {
      throw new JsonException();
    }
  }

  public override void Write(Utf8JsonWriter writer, Id value, JsonSerializerOptions options) =>
    writer.WriteNumberValue(value.Value);

}
lonix
  • 14,255
  • 23
  • 85
  • 176
1

The correct answer (as usual): it depends.

There's a special model binder to extract model from body payload.

As you can see, it uses a collection of input formatters. And depending on used Conent-Type header it will be Json-, Xml- or any other type of content serializer.

The fun fact is that model binder does not do any model validation completely delegating the task to input formatter. (This was a surprise for me to discover that NewtonJson ignores [Required] attribute, and I need to use [JsonProperty(Required = ...)]).

So, if you want to extract your model from the request's body, you need to do it for every Content-Type your application supports. By default, it's json only. Hence you need to create custom JsonConverter.

However, if the source of your model is route, query string parameters, or headers, then you may want to create custom model binder.

Pavel Voronin
  • 13,503
  • 7
  • 71
  • 137