Why to move to ASP.Net Core 2.1

ASP.Net 2.1 is packed with new features and improvements that make web development even more efficient but let’s see why to move to ASP.Net Core 2.1.

With the release of version 2.1 of ASP.Net Core, we get a new attribute named ApiController. As indicated by the API prefix and the way it is declared (see the code below), it is an attribute intended to be used only on a class and more specifically on a controller of a Web service API. This attribute is not useful on a controller returning Razor views.

[AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true)] 
public class ApiControllerAttribute : ControllerAttribute, IFilterMetadata, IApiBehaviorMetadata

In this article w,e will discuss the advantages obtained when using the ApiController attribute on our controller.

What we usually do

In the code below we see what a Web API controller usually looks like before the release of ASP.Net Core 2.1:

[Route("api/[controller]")]
public class PeopleController : ControllerBase
{
    private readonly IPeopleService peopleService;

    public PeopleController(IPeopleService peopleService)
    {
        this.peopleService = peopleService ?? throw new ArgumentNullException(nameof(peopleService));
    }

    [HttpGet]
    public async Task<ActionResult<List<Person>>> Get()
    {
        var people = await this.peopleService.GetAllAsync();
        return people;
    }

    [HttpGet("{id:int}", Name = "GetById")]
    public async Task<ActionResult<Person>> Get(int id)
    {
        var person = await this.peopleService.GetAsync(id);
        if (person == null)
        {
            return NotFound();
        }

        return person;
    }

    [HttpPost]
    public async Task<ActionResult<Person>> Post([FromBody]PersonDto dto)
    {
        if(!this.ModelState.IsValid)
        {
            return this.BadRequest(this.ModelState);
        }

        var person = new Person
        {
            LastName = dto.LastName,
            FirstName = dto.FirstName,
            Gender = dto.Gender
        };

        await this.peopleService.CreateAsync(person);

        return this.CreatedAtRoute("GetById", new { person.Id }, person);
    }

    [HttpPut("{id:int}")]
    public async Task<IActionResult> Put(int id, [FromBody]PersonDto dto)
    {
        if (!this.ModelState.IsValid)
        {
            return this.BadRequest(this.ModelState);
        }

        var person = await this.peopleService.GetAsync(id);
        if(person == null)
        {
            return NotFound();
        }

        person.FirstName = dto.FirstName;
        person.LastName = dto.LastName;
        person.Gender = dto.Gender;
        await this.peopleService.UpdateAsync(person);

        return this.NoContent();
    }


    [HttpDelete("{id:int}")]
    public async Task<IActionResult> Delete(int id)
    {
        var person = await this.peopleService.GetAsync(id);
        if (person == null)
        {
            return NotFound();
        }

        await this.peopleService.DeleteAsync(person);

        return this.NoContent();
    }
}

Our PersonDto class PersonDto like the code below:

public class PersonDto
{
    [Required, StringLength(20, MinimumLength = 2)]
    public string LastName { get; set; }

    [Required, StringLength(20, MinimumLength = 2)]
    public string FirstName { get; set; }

    public Gender? Gender { get; set; }
}

Avoid code duplication

One good reason to use this attribute is that it helps us avoid code duplication. If we look at the Post and Put actions of our PeopleController example controller, we notice that the first 3 lines of these actions are completely identical, namely that they contain the following instructions:

if (!this.ModelState.IsValid) { return this.BadRequest(this.ModelState); }

The condition this.ModelState.IsValid returns true if the model received as a parameter of the action is valid as validations rules applied using the data annotation attributes such as Required or the Validate method of the IValidatableObject interface (if it is impelmented) are verified successfully.

In our controller, the properties FirstName and FirstName are required and each must have its character number between 2 and 20 characters. If these properties or any of them do not respect the defined rules, we return the HTTP 400 code using the BadRequest method from the ControllerBase base class. So the user of our API web service will receive the following JSON content in return if it does not respect the validation rules:

{
    "LastName": [
        "The LastName field is required."
    ],
    "FirstName": [
        "The field FirstName must be a string with a minimum length of 2 and a maximum length of 20."
    ]
}

For our only controller, we repeat this code block twice in the same class because we need to be sure that the data when creating and modifying an entity is valid before continuing any processing. As part of a real project, for X entities we will have to repeat these 3 lines in the different actions where user input must be verified.

By decorating our controller with the ApiController attribute we will no longer need to write and duplicate this check block in different places. The verification is automatically handled by the attribute. If the data is invalid, it stops the execution of the action and returns the same HTTP 400 error code.

If you have already developed REST APIs with ASP.Net Web API technology or with an ASP.Net Core version earlier than version 2.1 and you do not like it too much then you may have had to define a filter to do exactly what the ApiController attribute ApiController . In my case, my filter with ASP.Net Web API 2 looks like the code below:

public class ModelStateValidationFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

If you are migrating your WEB API to ASP.Net Core version 2.1 then the previous filter is useless, it is dead code and less code means fewer tests to write and also less bugs likely to be treated on the part concerning the verification of the data at the level of the actions of our controllers.

Benefit from new default conventions on action parameter types
Still observing the Post and Put actions we notice that the parameters of these methods are decorated with the FromBody attribute. This is to specify that the relevant parameter data comes from the body of the HTTP request. In the absence of this ASP.Net Core attribute will attempt to do the data binding by searching the data through the parameter string of the HTTP request. Generally the data of the HTTP Post and Put methods are represented by a complex structure as a class so why not detect these complex types and automatically decide that the data binding must be done by looking at the body of the HTTP request? This will prevent us from polluting the declaration of the methods of our actions with attributes such as FromBody . The ApiController attribute allows you to benefit from certain conventions .

Here are the conventions applied if ASP.Net Core detects the presence of the ApiController attribute:

The FromBody attribute is automatically applied if the action parameter is a complex type. Some complex types have an exception like the IFormCollection type.
The FromForm attribute is automatically applied to the parameter if it is of the IFormFile or IFormFileCollection type.
The FromRoute attribute is used if the parameter name of the action method is the name of a template parameter defined as a route.
The FromQuery attribute usually for simple types like int , string etc.
By applying the ApiController attribute on our controller, the FromBody attribute can be removed on each of the Post and Put actions:

[HttpPost]
public async Task<ActionResult<Person>> Post(PersonDto dto)
{
    // [...]
}

[HttpPut("{id:int}")]
public async Task<IActionResult> Put(int id, PersonDto dto)
{
    // [...]
}

A small constraint.

The only constraint to using the ApiController attribute is that we have to go through the routing attributes to define the templates of our routes . This simply means that globally applied templates using app.UseMvc() or app.UseMvcWithDefaultRoute() will have no effect on the controller.

Personally, I have no problem going through the routing attributes. As mentioned above, this new attribute was created for the development of controllers used by the Web API services and often these are more or less following the REST architecture. Roughly speaking, if we have to respect the REST conventions, templates such as /api/people/{id:int}/addresses/{int:addressId}/ctiy/{int:cityId} are not easily set up by going through One or more global unique templates used through the app.UseMvc() or app.UseMvcWithDefaultRoute() . Most REST APIs developed with ASP.Net Core or ASP.Net Web API most commonly use routing attributes. So to force its use should just be considered, in my opinion, as a small constraint that will soon be forgotten.

The law of “all or nothing” does not exist
The two advantages mentioned so far do not follow the law of “all or nothing”. Some automatic options of the ApiController attribute can be disabled so you can opt for any of the benefits of the attribute.

For example, if you do not like the second benefit of automatically applying data binding conventions, ASP.Net Core allows you to disable this by going to the Startup class and then the ConfigureServices method and adding the lines. following:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressInferBindingSourcesForParameters = false;
});

There is also the SuppressModelStateInvalidFilter property. This is the option to disable the automation applied on the validation of the received user data.

Final version of our controller
Applying the ApiController attribute, here is what the final version of our controller used as an example in this article looks like:

[Route("api/[controller]")]
[ApiController]
public class PeopleController : ControllerBase
{
    private readonly IPeopleService peopleService;

    public PeopleController(IPeopleService peopleService)
    {
        this.peopleService = peopleService ?? throw new ArgumentNullException(nameof(peopleService));
    }

    [HttpGet]
    public async Task<ActionResult<List<Person>>> Get()
    {
        var people = await this.peopleService.GetAllAsync();
        return people;
    }

    [HttpGet("{id:int}", Name = "GetById")]
    public async Task<ActionResult<Person>> Get(int id)
    {
        var person = await this.peopleService.GetAsync(id);
        if (person == null)
        {
            return NotFound();
        }

        return person;
    }

    [HttpPost]
    public async Task<ActionResult<Person>> Post(PersonDto dto)
    {
        var person = new Person
        {
            LastName = dto.LastName,
            FirstName = dto.FirstName,
            Gender = dto.Gender
        };

        await this.peopleService.CreateAsync(person);

        return this.CreatedAtRoute("GetById", new { person.Id }, person);
    }

    [HttpPut("{id:int}")]
    public async Task<IActionResult> Put(int id, PersonDto dto)
    {
        var person = await this.peopleService.GetAsync(id);
        if(person == null)
        {
            return NotFound();
        }

        person.FirstName = dto.FirstName;
        person.LastName = dto.LastName;
        person.Gender = dto.Gender;
        await this.peopleService.UpdateAsync(person);

        return this.NoContent();
    }

    [HttpDelete("{id:int}")]
    public async Task<IActionResult> Delete(int id)
    {
        var person = await this.peopleService.GetAsync(id);
        if (person == null)
        {
            return NotFound();
        }

        await this.peopleService.DeleteAsync(person);

        return this.NoContent();
    }
}

 

You may also like...

Leave a Reply

Your email address will not be published.