Ant
Ant

Reputation: 191

Custom model binder for a value type - default constructor

I have the following domain type modelled via a value type:

public readonly record struct Id
{
    public string Value { get; }

    public Id() => throw new InvalidOperationException("Please use the 'Of' factory method.");

    public Id(string value)
        => this.Value = IsValid(value, out string? errorMessage) ? value : throw new ArgumentException(errorMessage);

    public static bool IsValid([NotNullWhen(true)] string? value, [NotNullWhen(false)] out string? errorMessage)
    {
        errorMessage = string.IsNullOrWhiteSpace(value) is false ? null : $"Invalid {nameof(ImportId)} value.";

        return errorMessage is null;
    }

    public static Id Of(string value) => new(value);
}

Please note:

  1. It's a value type and that's intentional - it must introduce as little overhead as possible compared to using just the bare type it wraps (string in this case).
  2. It's important that instances be created via the .Of factory method or via the constructor with parameters - to enforce 'correct by construction' semantics. So, an exception is thrown from the default constructor.

Now, I'm trying to use this type as a parameter of a controller in my web api:


    public record BeginImport(Id Id);

    [HttpGet]
    [Route("{id}")]
    public string Get(
        [ModelBinder(BinderType = typeof(ModelBinder), Name = $"id")]
        Request.BeginImport request)
    {
        return request.Id.Value; // just a stand-in
    }

To properly bind the parameter I use the following model binder:

public class ModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ArgumentNullException.ThrowIfNull(bindingContext);

        ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult == ValueProviderResult.None || valueProviderResult.FirstValue is null)
        {
            bindingContext.ModelState.TryAddModelError(
                bindingContext.ModelName, $"The {bindingContext.ModelName} field is required.");

            return Task.CompletedTask;
        }

        if (Id.IsValid(valueProviderResult.FirstValue, out string? errorMessage))
        {
            Id id = Id.Of(valueProviderResult.FirstValue);

            BeginImport beginImport = new(id);

            bindingContext.Result = ModelBindingResult.Success(beginImport);
        }
        else
        {
            bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, errorMessage);
        }

        return Task.CompletedTask;
    }
}

Now, the binder executes and does its job just fine assigning a correct value to the bindingContext.Result. But between the binder exiting and my controller getting to run, some code (in the asp.net core guts) makes a call to the Id's default constructor. And everything blows up :)

Two questions:

  1. Does anyone know/can explain why the runtime still needs to call Id's default constructor when all the needed job is done by a custom model binder?
  2. More importantly: is there any way to work this around and tell the runtime to leave the default constructor alone?

Thanks in advance.

Upvotes: 0

Views: 36

Answers (0)

Related Questions