Search code examples
c#value-objectsminimal-apisasp.net-core-8

ASP.NET Core 8 Minimal API [FromForm] Binding of a Value Object Property


I'm in the middle of converting an ASP.NET Core 6 API app to be a Minimal API app. Now that ASP.NET Core 8 supports [FromForm] for object binding, I decided to take the plunge, but I'm running into trouble.

Specifically I'm having a hard time getting value objects bound. In my case these are strongly typed ids using Andrew Lock's StronglyTypedId. When I added a TryParse static method to the value object, as mentioned here, it successfully bound when using [FromRoute], but I can't get it to bind to the property of an object, even though the TryParse should still apply.

Here's the object I'm trying to bind using [FromForm]:

public sealed class Command :
    IRequest<IResult> {
    [MaxLength(254), EmailAddress]
    public string? Email { get; init; }

    [Required]
    public ContactId Id { get; init; }//<-- The property not binding...

    [Required, MaxLength(byte.MaxValue)]
    public string Name { get; init; } = null!;

    [Range(1000000000, 9999999999)]
    public long? Phone { get; init; }
}

And here's the value object for the ContactId property:

[StronglyTypedId(StronglyTypedIdBackingType.Int, StronglyTypedIdConverter.EfCoreValueConverter | StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.TypeConverter)]
public partial struct ContactId {
    public static ContactId Parse(
        int? value) => value.HasValue
        ? new ContactId(value.Value)
        : throw new ArgumentNullException(nameof(value));

    public static ContactId Parse(
        ReadOnlySpan<char> value) => int.TryParse(value, out var intValue)
        ? new ContactId(intValue)
        : throw new ArgumentNullException(nameof(value));

    public static bool TryParse(
        string value,
        out ContactId id) {
        if (!int.TryParse(value, out var intValue)) {
            ArgumentNullException.ThrowIfNull(value, nameof(value));
        }

        id = new ContactId(intValue);

        return true;
    }

    public sealed class EfCoreValueGenerator :
        ValueGenerator<ContactId> {
        public override ContactId Next(
            EntityEntry entry) => new(Random.Shared.Next() * -1);

        public override bool GeneratesTemporaryValues => true;
    }
}

What should I do to resolve this?


Solution

  • After spending a day on this, I ended up resolving it by upgrading my value objects to use the StronglyTypeIds beta 7 source generator. After converting everything from beta 6 to 7, adding in my own templates for byte and short backed values, it all seems to be working as expected.

    I can only surmise that the new interfaces and methods that were introduced in .NET 7 and .NET 8 that the upgraded value objects use are what made the model binding work.

    Specifically one of the IFormattable, IParsable<T>, ISpanFormattable, ISpanParsable<T>, IUtf8SpanFormattable or IUtf8SpanParsable<T> interfaces and related method implementations probably did it.

    So, in conclusion, if you're using the StronglyTypedIds package, and you're implementing an ASP.NET Core 8+ Minimal API that uses [FromForm] for model binding, then make sure you're on StronglyTypedIds beta 7 or newer.