Custom Model Binders in ASP.NET Core
What are Model Binders?
ModelBinders are required in ASP.NET Core to pass parameters from HTTP requests into a controller action method.
For example, they ensure that an ID from a URL is available as a parameter in the method, taking into account the respective parameter type.
// users/1
public IActionResult GetById(int id) ..
Why custom model binders?
Own ModelBinders can be used very practically, e.g. to avoid repetitive code.
For example, very often the current user is needed in a web application; but maybe not every property of the user is needed, but only the id.
ASP.NET Core has ModelBinders for the types existing in .NET on board by default; you have to register your own. All standard ModelBinders can be overwritten or deactivated, but this is only necessary in special cases.
Sample
As an example I use a class that should represent the current id of the user. The Id comes from a default id-claim of the current ClaimIdentity.
Normally this would require the following code in each action:
public IActionResult MyAction()
{
int? userId;
var user = this.User;
var claims = user.Claims;
var idClaim = claims.SingleOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier);
if(idClaim != null)
{
if (int.TryParse(idClaim.Value, out int claimIdValue)
{
userId = claimIdValue;
}
}
// you can use userId here
// ...
}
## Binder Class
In order for a ModelBinder to become active at all, we need a separate class for it.
Thanks to the type, ASP.NET Core can later recognize which ModelBinder must be used.
Since our user id is always an integer, I gave the model some operators to make it easier to use against methods with int parameters.
```csharp
public class PortalUserId
{
public PortalUserId(int value)
{
Value = value;
}
public int Value { get; }
public static implicit operator PortalUserId(int value)
{
return new PortalUserId(value);
}
public static implicit operator int?(PortalUserId userId)
{
return userId?.Value;
}
public static implicit operator int(PortalUserId userId)
{
return userId.Value;
}
}
The goal is to have this class available at every necessary action as a parameter to avoid the constantly repeating code.
public IActionResult MyAction(PortalUserId userId)
{
// you can use userId here
// ...
}
The Model Binder
The ModelBinder ensures that ASP.NET knows what to do with each type in the action's parameter list - and what data and sources to use to create the object.
At this point, in-memory information (like here from the HTTP context) can be used as a source, just like databases or files.
However, it is recommended that this is done with a high performance.
I like to use this to centralize recurring queries, also with MediatR and asynchronous operations / notifications.
public class PortalUserIdBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
PortalUserId portalUserId = null;
int? userId = null;
// find user and the claims
var user = bindingContext.ActionContext.HttpContext.User;
var claims = user.Claims;
var idClaim = claims.SingleOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier);
// find claim and read value
if (idClaim != null)
{
if (int.TryParse(idClaim.Value, out int claimIdValue)
{
userId = claimIdValue;
}
}
// create object if id is valid
if (userId.HasValue)
{
portalUserId = PortalUserId.Create(userId.Value);
}
// if user id was found, the filled object is passed
// if userId was not found, null will be passed to the actions
bindingContext.Result = ModelBindingResult.Success(portalUserId);
return Task.CompletedTask;
}
}
Register the ModelBinder
For ASP.NET Core to even know the binder, it must be registered through a binder provider.
This is where the mapping between binder and type takes place.
If you should have several binders, because you want to save a lot of code in many actions, then you can easily register several binders here.
public class PortlModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context is null) throw new ArgumentNullException(nameof(context));
// our binders here
if (context.Metadata.ModelType == typeof(PortalUserId))
{
return new BinderTypeModelBinder(typeof(PortalUserIdBinder));
}
// your maybe have more binders?
// ....
// this provider does not provide any binder for given type
// so we return null
return null;
}
}
Then the BinderProvider must also be made known.
public void ConfigureServices(IServiceCollection services)
{
// ....
services.AddControllersWithViews(o =>
{
// adds custom binder at first place
o.ModelBinderProviders.Insert(0, new PortlModelBinderProvider());
}).AddRazorRuntimeCompilation();
// ....
}
Ready is our binder structure, which now saves us a lot of code in our actions :-)