Back

A clean way to use required value types in ASP.NET Core

TL;DR

How do you deal with required non-nullable value types (e.g. integers) in input models? Using int? everywhere is ugly, so what can we do? In short:

  • For simple cases, use [BindRequired] instead of [Required]. Additionally, create a metadata provider for the model binders.
  • When you also need to bind data from the body (JSON), use the required keyword.
  • If you want to improve the JSON model errors, create a custom value provider.
  • And if you want custom property-specific error messages, implement a custom model binder. This is the most complicated and comprehensive solution.

You can also check out the source code for an example project that utilizes the final approach. Note that the implementation is partial and doesn’t support JSON arrays.

Table of contents

The problem.

Say we have an input model for a person with mandatory name and age fields:

public record PersonInputModel
{
    [Required] public string Name { get; init; } = null!;
    [Required] public int Age { get; init; }
}

If you try this code out, you’ll find that the age field isn’t actually required. This is because when a property is not set by the user, the default value of the data type is used instead. For value types like integers and dates, it means 0 and January 1, 0001 00:00:00.

Using the [Required] data attribute or a validation library like FluentValidation won’t help, as neither can distinguish between the following cases:

  1. The user not providing a value whatsoever
  2. The user explicitly providing the default value, e.g. the number zero for integers

The [Required] attribute works fine with reference types like strings and records, as their default values are null. This, in fact, is how [Required] works: it simply checks whether a property is null or something like an empty string.

The quick fix.

Our first instinct might be to settle for something uglier, and in many cases, this is fine.

public record PersonInputModel
{
    [Required] public string Name { get; init; } = null!;
    [Required] public int? Age { get; init; }
}

The age integer is now a nullable value type Nullable<int> and its default value is null. Our name string is a reference type and can stay as it is.

Alas, we now have to deal with a few annoying problems:

  1. We’ll get null warnings from the compiler whenever we try to access the properties we know can’t truly be null (or instead, we’ll have to use this attribute everywhere)
  2. We have to cast our value types whenever we want to pass the integers to methods that don’t accept nullable types
  3. The input model itself is less semantically expressive: what’s to distinguish the mandatory ID code from the truly optional age field?
  4. This shouldn’t be a concern of our application code to begin with: it’s an implementation leak. Required value types are a topic that often confuse those new to ASP.NET

The solution.

This reason for these limitations is that regular validation happens after model binding and has no access to any binding information.

There’s multiple ways to solve this problem. The one you should choose depends on your use case. I’ll go over them from the simplest to the most complex.

BindRequired

Required property validation should ideally be done during the binding process itself, where it is still possible to distinguish between a property not being set and being explicitly set to its default value. And that’s exactly what the [BindRequired] attribute does.

public record PersonInputModel
{
    public string Name { get; init; } = null!;
    [BindRequired] public int Age { get; init; }
}

This will make the model binder return an error if the ID code is not set. However, this solution has a few drawbacks:

  1. It doesn’t work with body data such as JSON (read more here)
  2. It doesn’t allow you to specify custom error messages
  3. You’ll have to use data attributes for validation

You can also create a class that will automatically make certain properties bind-required. For example, here is the most common use case: adding bind-required functionality to properties that have the [Required] attribute.

public class RequiredBindingMetadataProvider : IBindingMetadataProvider
{
    public void CreateBindingMetadata(BindingMetadataProviderContext context)
    {
        if (context.PropertyAttributes?.OfType<RequiredAttribute>().Any() is true)
        {
            context.BindingMetadata.IsBindingRequired = true;
        }
    }
}

The benefit of this metadata provider is that we’ll no longer have to think about whether to use [Required] or [BindRequired] in our code all the time; the implementation detail is abstracted away.

To use the metadata provider, add this configuration:

builder.Services.Configure<MvcOptions>(options => {
    options.ModelMetadataDetailsProviders.Add(new RequiredBindingMetadataProvider());
});

Later on, we’ll see how to require binding implicitly through other conditions than the [Required] attribute, as well as how to customize the error messages.

JSON value binding

The reason that you can’t require binding for body data is that the default BodyModelBinder doesn’t support it. The body model binder works by deserializing and binding the entire JSON object at once. Lucky for us, the default JSON serializer supports the required keyword from C# 11 onward.

public record LocationInputModel {
    public required int Latitude { get; init; }
    public required int Longitude { get; init; }
}

This will make the JSON serializer return an error if a required property is not found in the JSON object. But the error messages it returns are ugly:

{
    "$": [
        "JSON deserialization for type 'MySolution.PersonAtLocationInputModel' was"
        "missing required properties, including the following: latitude, longitude"
    ],
    "inputModel": ["The inputModel field is required."]
}

Not only this, but in case of nested input models, the serializer will stop at the first error it encounters. This is important if you want to show the user all the validation errors at once.

Better JSON errors

To solve the issue of bad error messages, you can implement a custom value provider. Value providers are used by model binders in order to acquire values associated with properties from the request object, one-by-one.

Most default model binders work with route & query string data through their respective value providers. For example, RouteValueProvider checks whether there is a route variable with a name matching the property.

But there isn’t a value provider for JSON. Instead, you have BodyModelBuilder.

BodyModelBinder is weird. It short-circuits the value-providing process for body data and sets the entire input model at once directly through a JSON serializer (you can read the source here). Simply put, it doesn’t use value providers at all.

So let’s disable the body model binder:

builder.Services.Configure<MvcOptions>(options =>
{
    options.ModelBinderProviders.RemoveType<BodyModelBinderProvider>();
});

And now that it won’t be causing any more trouble, let’s allow the remaining model binders to do their job through our brand new JSON Value Provider™. PS: I never got to adding the logic required for JSON arrays, as that was outside my use case. It shouldn’t be too complicated, however.

public class JsonValueProvider : IValueProvider
{
    private JsonDocument? Json { get; }

    public JsonValueProvider(string body)
    {
        try
        {
            Json = JsonDocument.Parse(body);
        } catch (JsonException) { }
    }

    public bool ContainsPrefix(string prefix)
    {
        return FindElement(prefix) is not null;
    }

    public ValueProviderResult GetValue(string key)
    {
        var result = FindElement(key);

        if (result is null) {
            return ValueProviderResult.None;
        }

        return new ValueProviderResult(new(result));
    }

    private string? FindElement(string key)
    {
        if (Json is null)
        {
            return null;
        }

        var element = Json.RootElement;

        foreach (var path in key.Split('.'))
        {
            var current = element;
            if (!current.TryGetProperty(path.ToLower(), out element))
            {
                if (!current.TryGetProperty(path, out element))
                {
                    return null;
                }
            }
        }

        return element.ToString();
    }
}

This value provider deserializes the body JSON string into a JsonDocument object, and whenever the model binder asks for a value, it searches for the corresponding property in the JSON object. This might be slower than deserializing the entire JSON at once like BodyModelBinder does, but makes body data binding implementation work the same way as route and query data.

You’re also going to need a factory that creates these value providers for each request. Its job is to handle encoding and get the JSON string from the request body, passing it to the provider. It’s rather self-explanatory, so proof is left to the reader.

public class JsonValueProviderFactory : IValueProviderFactory
{
    public async Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        var body = await GetBody(context.ActionContext.HttpContext);
        var provider = new JsonValueProvider(body);
        context.ValueProviders.Insert(0, provider);
    }

    private static async Task<string> GetBody(HttpContext httpContext)
    {
        var encoding = GetEncoding(httpContext);
        var inputStream = GetInputStream(httpContext, encoding);

        using var reader = new StreamReader(inputStream);
        var body = await reader.ReadToEndAsync()
                               .ConfigureAwait(continueOnCapturedContext: false);

        return body;
    }

    private static Stream GetInputStream(HttpContext httpContext, Encoding encoding)
    {
        if (encoding.CodePage == Encoding.UTF8.CodePage)
        {
            return httpContext.Request.Body;
        }

        return Encoding.CreateTranscodingStream(httpContext.Request.Body,
                                                encoding,
                                                Encoding.UTF8,
                                                leaveOpen: true);
    }

    private static Encoding GetEncoding(HttpContext httpContext)
    {
        var contentType = httpContext.Request.ContentType;

        var mediaType = string.IsNullOrEmpty(contentType)
            ? default
            : new MediaType(contentType);

        var encoding = mediaType.Charset.HasValue ? mediaType.Encoding : null;
        return encoding ?? Encoding.UTF8;
    }
}

Once you’re done drawing the rest of the owl, throw it onto the pile:

builder.Services.Configure<MvcOptions>(options =>
{
    options.ModelMetadataDetailsProviders.Add(new RequiredBindingMetadataProvider());
    options.ModelBinderProviders.RemoveType<BodyModelBinderProvider>();
    options.ValueProviderFactories.Insert(0, new JsonValueProviderFactory());
});

Inserting the value provider factory at the start of the list ensures that it will be used before any other value providers.

Now you can use the [Required] attribute to make your value types required through RequiredBindingMetadataProvider, even with JSON data.

Custom error messages

The previous solution works, but we still can’t customize the error messages through [Required(ErrorMessage = "fnord")]. Metadata providers nor value providers can access the attributes of the input model properties. To access this data, we’re going to need a custom model binder and model binder provider.

The model binder is responsible for checking whether a value is present in the request and returning an error if it isn’t.

public class ValueTypeModelBinder : IModelBinder
{
    private readonly IModelBinder _defaultBinder;

    public ValueTypeModelBinder(IModelBinder defaultBinder)
    {
        _defaultBinder = defaultBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext is null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var field = bindingContext.ModelName; // e.g. a property called "Id"
        var value = bindingContext.ValueProvider.GetValue(field);

        var metadata = bindingContext.ModelMetadata.ValidatorMetadata;
        var requiredAttribute = metadata.OfType<RequiredAttribute>().FirstOrDefault();

        bool isRequired = requiredAttribute is not null;

        if (isRequired && value == ValueProviderResult.None)
        {
            var defaultError = $"The {field} field is required.";
            var error = requiredAttribute?.ErrorMessage;
            bindingContext.ModelState.TryAddModelError(field, error ?? defaultError);
            return;
        }

        bindingContext.ModelState.SetModelValue(field, value);

        // delegate to the default binder
        // bit hacky, but necessary workaround
        await _defaultBinder.BindModelAsync(bindingContext);
    }
}

For each non-nullable value-typed property on the input model, the model binder asks all the registered value providers whether they have a value for the given property with GetValue(field). This works thanks to our custom JSON value provider.

If a value exists, the actual work of binding the value to the property is delegated to a default model binder most suitable for the data type through _defaultBinder.BindModelAsync(bindingContext). The default binder is the model binder that would’ve been called if ours hadn’t intercepted the binding of the property.

In case no value providers find a property, meaning it’s missing entirely, an error will be added to the model state with a custom error message acquired through attribute reflection.

Once we’re done with the model binder, we also have to create the model binder provider. The model binder provider decides when our model binder should be used in the first place.

For every property on the input model, ASP.NET calls all its registered model binder providers until one of them returns a model binder instead of null. In our case, we want to return our model binder whenever a property is a non-nullable value type.

public class ValueTypeModelBinderProvider : IModelBinderProvider
{
    private readonly IList<IModelBinderProvider> _providers;

    public ValueTypeModelBinderProvider(IList<IModelBinderProvider> providers)
    {
        _providers = providers;
    }

    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var type = context.Metadata.ModelType;

        bool isValueType = type.IsValueType;
        bool isNullable = Nullable.GetUnderlyingType(type) is not null;

        if (isValueType && !isNullable)
        {
            var defaultBinder = _providers.Where(x => x.GetType() != GetType())
                                          .Select(x => x.GetBinder(context))
                                          .FirstOrDefault(x => x is not null);

            if (defaultBinder is null)
            {
                throw new InvalidOperationException("Value type can't be bound.");
            }

            return new ValueTypeModelBinder(defaultBinder);
        }

        return null;
    }
}

If we’re dealing with a non-nullable value type, we return our custom model binder. Otherwise, we return null and ASP.NET calls the next model binder provider’s GetBinder() method.

The special part of this model binder provider is the _providers variable. Upon constructing the provider during configuration, we have to pass in a list of all the other model binder providers:

builder.Services.Configure<MvcOptions>(options =>
{
    options.ValueProviderFactories.Insert(0, new JsonValueProviderFactory());
    options.ModelBinderProviders.RemoveType<BodyModelBinderProvider>();
    options.ModelBinderProviders.Insert(0,
        new ValueTypeModelBinderProvider(options.ModelBinderProviders));
});

As you can see, we pass in options.ModelBinderProviders and once again pass ours to the start of the list, so ASP.NET will call it before any other model binder providers.

Since we no longer use bind-required metadata functionality, but instead check for missing properties through the value binder, you can remove the RequiredBindingMetadataProvider metadata provider.

Summary

Let’s recap:

  • We created the JSON value provider JsonValueProvider that can search for fields in the body of a request.
  • We created a factory for the JSON value provider, JsonValueProviderFactory, that can get the JSON string from the request body.
  • We registered the factory with ASP.NET so it gets called by model binders.
  • We removed the default body model binder provider, BodyModelBinderProvider, as it interferes with our custom JSON value provider.
  • We created a custom model binder, ValueTypeModelBinder, that can check whether a value is present in the request and return a custom error if it isn’t.
  • We created the model binder provider, ValueTypeModelBinderProvider, that makes ASP.NET use our model binder only when binding non-nullable value types.
  • We registered the model binder provider and prioritized it over all the other ones, so it gets called first.

Farewell.

That’s about it. It took me a while to figure out all of the moving pieces, as most of this information doesn’t exist online in a digestible form. Hence I thought I’d share it with the rest of the internet. The entire source and a sample project are available here.