46  
netcore
Advertisement
Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jan 18, 2023

ASP.Net Core Complex Model Binding

Автор:
Erick Gallani
Источник:
Просмотров:
1746

Did you ever face a situation where you have a search UI, like a front-end application, that has a data table and you need to support a complex query construction that can contain Free Text search, Sorting of multiple columns, and extra filters and query parameters starts to look a nightmare on your controllers?

ASP.Net offers great flexibility, extensibility, and customization to graceful handle these situations.

The problem

Let’s imagine the scenario where you need to create a Search API to provide data to a Data Table in a UI, and this search needs to allow free text search, multiple sortings for different properties, and add any other extra piece of filtering property.

API Contract

So you have an API Contract something like this.

{
  "term": "cool stuff",
  "countryAlphaCode2": "US",
  "sorts": [
     "asc|percentualDelivered",
     "desc|percentualPaid"
  ]
}

So as you can see there, the sort will be sent to the API following the pattern {direction[asc|desc]}|{propertyName} which is a fair pattern and easy for URL query parameters to send over.

But the problem with this approach is if we just received the parameters as pure strings on the controller, this will pollute our controller, we’ll not use the advantage of the binding pipeline for fail-fast for example, and it will demand a propagation of raw string to be parsed and interpreted.

Another problem is that every time a new query parameter is introduced a lot of changes will be required on the controller action signature and the code inside.

Controller using primitives only

[HttpGet]
public async Task<IActionResult> SearchAsync([FromQuery] string term, [FromQuery] string countryAlphaCode2, [FromQuery] string[] sorts, CancellationToken cancellation)

How the request for this example above would look like?

https://my-api.com/search?term=cool stuff&countryAlphaCode2=US&sorts[0]=asc|percentualDelivered&sorts[1]=desc|percentualPaid

Parsed URI request

The query parameter actually needs to be encoded before the request, so you will see something like the below.

https://my-api.com/search?term=cool%20stuff&countryAlphaCode2=US&sorts%5B0%5D=asc%7CpercentualDelivered&sorts%5B1%5D=desc%7CpercentualPaid

• • •

Now let’s take a step forward and create a class that will represent the query parameters.

Search query parameter class

public class Search
{
    [BindProperty(Name = "term")]
    public string SearchTerm { get; set; }

    [BindProperty(Name = "countryAlphaCode2")]
    public string CountryAlphaCode2 { get; set; }

    [BindProperty(Name = "sorts")]
    public Sort[] Sorts { get; set; }
}

As you may notice instead of an array of strings in the Sorts we’ll use a class to interpret the string pattern {direction[asc|desc]}|{propertyName} and create a complex type instead of an array of primitives (strings).

The problem here is that if we just add a class there will not work quite well because by default a complex type model binder bind values direct to the properties, by Microsoft documentation.

A complex type must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor.

Which in this case will not help us too much because having a class with a public setter that holds an array of strings will bring no advantages at all.

Wouldn’t be great if we could have a class that will hold all the “sort” pattern parse logic, receiving the sort string in the constructor and building it in a complex meaningful type with validations and more enriched values?

Well, in fact, we can with the Custom Model Binding.

So let’s create a ModelBinder class and also our Sort class.

Sort Model Binder

public class SortModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        bindingContext.Result = ModelBindingResult.Success(new Sort(valueProviderResult.FirstValue));

        return Task.CompletedTask;
    }
}

Sort Query class

[ModelBinder(BinderType = typeof(SortModelBinder))]
public record Sort
{
    private const char ParameterDelimiter = '|';
    private const uint DirectionIndex = 0;
    private const uint PropertyIndex = 1;
    private const uint AllowedItemsCount = 2;
    private const string InvalidCastExceptionMessage = "Incorrect sort query string format, the format should be {direction[asc|desc]}|{property}";

    private const string APIDescNameContract = "desc";
    private const string APIAscNameContract = "asc";

    private static string[] Directions = new[] { APIDescNameContract, APIAscNameContract };

    public Sort(string sortQueryString)
    {
        if (sortQueryString is null || sortQueryString.IndexOf(ParameterDelimiter) < 0)
            throw new InvalidCastException(InvalidCastExceptionMessage);

        var splitString = sortQueryString.Split(ParameterDelimiter,
            StringSplitOptions.RemoveEmptyEntries);

        if (splitString.Length != AllowedItemsCount)
            throw new InvalidCastException(InvalidCastExceptionMessage);

        Property = splitString[PropertyIndex].Trim().ToLowerInvariant();

        var direction = splitString[DirectionIndex].Trim().ToLowerInvariant();

        if (!Directions.Any(dir => dir == direction))
            throw new InvalidCastException("Sort direction only allows 'desc' or 'asc'");

        switch (direction)
        {
            case APIDescNameContract:
                Direction = SortDirection.Descending;
                break;
            case APIAscNameContract:
                Direction = SortDirection.Ascending;
                break;
            default:
                throw new InvalidCastException("Sort Direction not valid");
        }
    }

    public SortDirection Direction { get; private set; }
    public string Property { get; private set; }
}

 

public enum SortDirection
{
    Ascending,
    Descending
}

And this is how it works.

When data comes from an HTTP request ASP.Net framework will loop over all the parameters in the controller action signature looking through the available sources in the HTTP request and trying to find matching names between the parameters and the query params in our example (remember parameters can come [FromQuery], [FromBody], [FromRoute], [FromForm], and [FromHeader]) and try to parse the received value to the controller parameter type.

So if we want .Net to do more fancy work for us we need to instruct the framework on how to do it, and that’s basically what the code above is doing.

SortModelBinder is used on the Sort class declaration [ModelBinder(BinderType = typeof(SortModelBinder))] to instruct .Net that, for building the Sort class it needs to run our custom binding algorithm and our algorithm will instantiate a Sort class for each string value of the Sorting array received on the request.

So now we can do a lot of complex work delegated to the framework, like on the Sort class where we receive a string in the constructor and actually do a series of validations for a fail-fast approach, convert the string direction “asc” and “desc” to an enum “Ascending” and “Descending”, standardizing the string to lower case, respecting the single principle from SOLID by creating classes that only have one reason to change, and easy to unit test.

And finally…

Controller using a complex type model binding

[HttpGet]
public async Task<IActionResult> SearchAsync([FromQuery] Search querySearch, CancellationToken cancellation)

Is way better don’t you think? Cleaner and a better design to use the strength of the framework in our favor.

Conclusion

As you can see ASP.Net is a very powerful and flexible framework with a lot of extensible features to make your code more readable, testable, flexible, and enriched.

Reference

Microsoft Documentation: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-6.0

In this post, I’ll cover how can we use this alongside Entity Framework Core and Linq Expression Tree to easily add sorting dynamically to a Queryable object using this binding we just created interacting with the Database.

Похожее
Nov 25, 2021
Author: Jay Krishna Reddy
Today, we are going to cover uploading and downloading multiple files using ASP.Net Core 5.0 Web API by a simple process.Note: This same technique works in .Net Core 3.1 and .Net Core 2.1 as well.Begin with creating an empty web...
Apr 24, 2022
Author: Dr Vin
Let’s find out which one is better .Net 6 or .Net 5 and how .Net has changed the game of software development.In the past, I have briefly talked about the Dot Net 6 framework that was released last year in...
Feb 12
Author: Kartik
Application Insights - Telemetry1. How do I instrument (monitor/record/enabling to capture telemetry) an application?Autoinstrumentation - if you don’t have access to source codeYou only need to install the Application Insights SDK if: You require custom events and...
Jun 3, 2021
Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the same-origin policy. The same-origin policy prevents a malicious site from reading sensitive data from...
Написать сообщение
Почта
Имя
*Сообщение


© 1999–2024 WebDynamics
1980–... Sergey Drozdov
Area of interests: .NET Framework | .NET Core | C# | ASP.NET | Windows Forms | WPF | HTML5 | CSS3 | jQuery | AJAX | Angular | React | MS SQL Server | Transact-SQL | ADO.NET | Entity Framework | IIS | OOP | OOA | OOD | WCF | WPF | MSMQ | MVC | MVP | MVVM | Design Patterns | Enterprise Architecture | Scrum | Kanban