DEV Community

Brandon Ventura
Brandon Ventura

Posted on

ApiControllerAttribute en .NET Core

Texto contenido en la imagen que dice ASP .NET Core

Muchas veces dentro del desarrollo de Web APIs con .NET Core nos encontramos utilizando notaciones, recursos, atributos que vienen disponibles con el entorno y esto lo realizamos de manera prácticamente automática, en general, esto habla muy bien del trabajo por detrás del equipo encargado del mantenimiento de .NET, ya que como desarrolladores nos mantiene aislados de preocupaciones técnicas para enfocarnos en el desarrollo de funcionalidades del negocio.

Uno de estos pequeños componentes es el atributo ApiControllerAttribute que colocamos tan mecánicamente a nuestros controladores de API nada más crearlos, siendo una de las bondades disponibles y que en lo personal aportan tanto a la productividad en el desarrollo de Web APIs y uno de los que más me hacen falta cuando me encuentro dando mantenimiento a aplicaciones construidas sobre .NET Framework.

Antes de ApiControllerAttribute

Antes de .NET Core en .NET Framework no disponíamos de la clase o atributo ApiControllerAttribute, en realidad, no existe nada parecido en ASP .NET Framework (aunque podemos escribir filtros de acciones para lograr el mismo objetivo) y es importante no confundir este atributo con la clase ApiController en .NET Framework ya que esta clase proporciona la funcionalidad básica para procesar solicitudes HTTP a través de un conjunto de métodos y funcionalidades relacionadas con la escritura de controladores de Web API en .NET Framework más que modificar el comportamiento por defecto de estos controladores.

En ASP .NET Framework tenemos disponibles las clases Controller y ApiController cada una utilizada según el tipo de proyecto, Controller para controladores que principalmente realizan tratamiento de datos para el renderizado de vistas y ApiController para tratamiento de datos como servicios web o Web APIs.

Sin embargo, en .NET Core desde su primera versión vino con una sola clase unificada para los controladores, esta es la clase Controller que combina las clases Controller, ApiController y AsyncController de ASP .NET Framework MVC y Web API. Además de la clase Controller .NET Core añade la clase ControllerBase de la cual hereda Controller. ControllerBase provee la funcionalidad básica para manejo de solicitudes HTTP pero no incluye el soporte de vistas y todo lo relacionado con estas que sí tiene la clase Controller.

Si lo ponemos sobre el papel en .NET Core, la clase ControllerBase sería lo mismo que ApiController y Controller lo mismo que la clase Controller de .NET Framework, salvando las grandes diferencias que hay.

La clase ApiController estuvo disponible en .NET Core hasta la versión 2.2 y además, por si te da curiosidad, sí, en ASP .NET Framework existe una clase llamada ControllerBase que contiene lo mínimo necesario para todos los controladores de vistas, pero no es una clase utilizada para crear controladores que respondan a solicitudes HTTP directamente.

Entonces, ¿Para que sirve el ApiControllerAttribute?

Agregar la anotación o atributo ApiControllerAttribute a los controladores, nos brinda un conjunto de características y comportamientos que ayudan a mejorar el trabajo de los desarrolladores dentro de estos, reduciendo la cantidad de código innecesario o repetitivo dentro del controlador y sus acciones.

Agregar el atributo nos brinda la capacidad de: enrutamiento, validación automática/implícita de modelos, parameter binding mediante inferencia, entre otras cosas.

¿Cómo se utiliza?

Tenemos tres formas de utilizar el atributo:

  1. Controladores individuales.
  2. Desde una clase base.
  3. Al ensamblado completo (Assembly).

Controladores individuales

Para agregarlo a nuestros controladores individuales debemos colocarlo sobre la definición de la clase del controlador, esto habilitará las características que dispone el atributo para nuestro controlador.

[ApiController]
[Route("[controller]")]
public class PersonasController: ControllerBase
{
    // Endpoints
}
Enter fullscreen mode Exit fullscreen mode

Con una clase base

Debemos crear una clase que disponga del uso del atributo y herede de ControllerBase, de esta manera podremos heredar nuestra clase base a múltiples subclases y así disponer el atributo a más de un solo controlador sin necesidad de constantemente colocarlo sobre la definición del tipo de nuestro controlador.

// Clase base
[ApiController]
public class AppBaseController: ControllerBase
{
}

// Subclase
[Route("[controller]")]
public class PersonasController: AppBaseController
{
    // Endpoints
}
Enter fullscreen mode Exit fullscreen mode

A todo el ensamblado

Por último, podemos aplicar el atributo a nivel de ensamblado, lo que lo agregara el atributo a todos los controladores del ensamblado; no se pueden excluir controladores específicos:

// Program.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;

[assembly: ApiController]
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

RouteAttribute

Si agregamos el atributo ApiControllerAttribute para decorar nuestros controladores, obligatoriamente debemos utilizar el atributo RouteAttribute para indicar la ruta a la que responderán las acciones del controlador.

[ApiController]
[Route("[controller]")]
public class PersonasController: ControllerBase
Enter fullscreen mode Exit fullscreen mode

No podemos dejar que la ruta se defina mediante las rutas convencionales del método UseEndpoints, UseMvc o UseMvcWithDefaultRoute.

Validación Automática de Modelo

Cuando utilizamos el atributo [ApiController] todos los errores de validación, tipado y restricciones de nuestros modelos se aplican automáticamente devolviendo una respuesta HTTP 400 Bad Request; de esta forma la validación es implícita en lugar de explícita, es decir, no necesitamos agregar código que verifique si nuestro modelo es correcto:

[HttpPost]
public ActionResult CreatePets([FromBody]Pet pet)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    return Ok(pet);
}
Enter fullscreen mode Exit fullscreen mode

Permitiendo eliminar así las siguientes líneas de las acciones de nuestro controlador:

if (!ModelState.IsValid)
        return BadRequest(ModelState);
Enter fullscreen mode Exit fullscreen mode

En especial esto es lo que más aporta en cuanto a productividad cuando creamos nuestros controladores, ya que además de evitar repetir estas mismas líneas de código una y otra vez, evitamos que nuestros modelos pasen sin validar en caso que olvidáramos verificar si el ModelState es valido o no.

Si nosotros no validamos nuestro modelo explícitamente y tampoco hacemos uso del atributo ApiControllerAttributeASP .NET Core no nos alertará de que nuestro modelo es inválido o no contiene la información que debería.

Respuesta con código 400 ejemplo básico

La imagen anterior representa una respuesta de validación utilizando la validación explícita o manual. Si nosotros agregamos el atributo ApiControllerAttribute se ve de la siguiente manera:

Imagen con código de estado 400 con ValidationProblemDetails

Este mensaje de error es la respuesta HTTP 400 predeterminada utilizando el comportamiento definido por defecto gracias al ApiControllerAttribute. El tipo que utiliza es ValidationProblemDetails en lugar de BadRequestResult.

El tipo ValidationProblemDetails proporciona un formato que especifica los errores en las respuestas con mucho mayor detalle para humanos y máquinas y además cumple los requisitos establecidos por la RFC 7807.

Por ello, si realizamos validaciones personalizadas en las que tengamos que hacer uso del ModelState en nuestras acciones, es más conveniente hacer uso del método ValidationProblem(ModelState) en lugar de BadRequest(ModelState), o simplemente ValidationProblem() ya que usara la propiedad ModelState del controlador por defecto, de esta forma mantendremos todas nuestras respuestas consistentes:

if (!ModelState.IsValid)
    return ValidationProblem(ModelState);
Enter fullscreen mode Exit fullscreen mode

A continuación puedes observar lo mencionado anteriormente:

[ApiController]
[Route("[controller]")]
public class PersonasController: ControllerBase
{

    /// <summary>
    /// Utiliza la validación explícita o manual del ModelState retornando un <see cref="BadRequestResult" />.
    /// </summary>
    [HttpPost("CrearPersona1")]
    public ActionResult CrearPersona1([FromBody] Persona persona)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        return Ok(persona);
    }

    /// <summary>
    /// Utiliza la validación explícita o manual del ModelState retornando un <see cref="ValidationProblemDetails" />
    /// </summary>
    [HttpPost("CrearPersona2")]
    public ActionResult CrearPersona2([FromBody] Persona persona)
    {
        if (!ModelState.IsValid)
            return ValidationProblem(); // es igual a return ValidationProblem(ModelState);

        return Ok(persona);
    }

    /// <summary>
    /// Deja que el atributo <see cref="ApiControllerAttribute" /> se encargue del comportamiento relacionado
    /// a la validación del ModelState.
    /// </summary>
    [HttpPost("CrearPersona3")]
    public ActionResult CrearPersona3([FromBody] Persona persona)
    {
        return Ok(persona);
    }
}
Enter fullscreen mode Exit fullscreen mode

Invalid Model Response Customization

Si la respuesta estándar por defecto de los errores de validación aplicada mediante ValidationProblemDetails no cumple con nuestras necesidades o incluso queremos personalizarla, también lo podemos hacer.

Para ello debemos modificar el “comportamiento” de devolución de estas respuestas en nuestra API. ASP .NET Core provee una configuración especial para modificar el comportamiento de diversos aspectos de nuestra API, esto a partir del método ConfigureApiBehaviorOptions() sobre AddControllers() en los servicios de nuestra API.

En las opciones de configuración del método ConfigureApiBehaviorOptions() tendremos disponible la propiedad InvalidModelStateResponseFactory que recibe un delegado o callback mediante el cual podremos indicar nuestra respuesta personalizada.

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var newResponse = new
            {
                prop1 = "Propiedad 1",
                prop2 = "Propiedad 2"
            };
            return new BadRequestObjectResult(newResponse);
        };
    });
Enter fullscreen mode Exit fullscreen mode

InvalidModelStateResponseFactory recibe de entrada un ActionContext que contiene la información del contexto para la ejecución de la acción que ha sido parte de esa solicitud HTTP, además, este delegado devuelve o debe devolver un IActionResult que determinara nuestra respuesta, en este caso un BadRequestObjectResult:

Ejemplo de InvalidModelStateResponseFactory básico

Lo anterior es un ejemplo básico, pero podemos personalizarlo a nuestro gusto, por ejemplo:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var newResponse = new
            {
                path = context.HttpContext.Request.Path.ToString(),
                httpMethod = context.HttpContext.Request.Method,
                resource = (context.ActionDescriptor as ControllerActionDescriptor)?.ControllerName,
                endpoint = (context.ActionDescriptor as ControllerActionDescriptor)?.ActionName,
                errors = context
                    .ModelState
                    .ToDictionary(a => a.Key, a => a.Value?.Errors.Select(e => e.ErrorMessage))
            };
            return new BadRequestObjectResult(newResponse);
        };
    });
Enter fullscreen mode Exit fullscreen mode

A partir del contexto ActionContext podemos obtener información referente al contexto HTTP de la solicitud, datos sobre el controlador y la acción ejecutada, el método de acción, la ruta y el ModelState para realizar el mapeo de la información de nuestro modelo, en este caso, la respuesta para un modelo inválido.

Ejemplo de InvalidModelStateResponseFactory completo

Al crear un nuevo delegado estamos sobrescribiendo la configuración por defecto de la propiedad InvalidModelStateResponseFactory.

Por defecto, utiliza la clase ProblemDetailsFactory que prácticamente devuelve una respuesta a partir de una instancia de ValidationProblemDetails.

Además, podemos incluir servicios dentro de este delegado, gracias a que ActionContext contiene el HttpContext de la solicitud, podemos solicitar los servicios disponibles dentro del contenedor de servicios de la solicitud, por ejemplo, podemos obtener el servicio de registro/trace/logs para guardar información referente a los errores de validación del modelo:

var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Hola desde modelo inválido!.");
Enter fullscreen mode Exit fullscreen mode

También podemos desactivar estas respuestas automáticas completamente si configuramos la propiedad SuppressModelStateInvalidFilter a true.

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });
Enter fullscreen mode Exit fullscreen mode

Source Binding Inference Rules

Cuando nosotros aplicamos el atributo ApiControllerAttribute sobre nuestros controladores, esté provee una característica que permite inferir/determinar el origen/fuente de donde provienen algunos parámetros de acción sin la necesidad que nosotros como desarrolladores debamos colocar explícitamente el origen de los datos utilizando los atributos de enlace de datos, como puede ser FromBody, FromForm, FromQuery, etc.

ApiControllerAttribute aplica las siguientes reglas de inferencia:

Binding Attribute Regla de Inferencia Origen de enlace
[FromBody] Inferido para cualquier tipo complejo, por ejemplo, las clases que nosotros mismos creamos. Si una acción hace referencia por inferencia o explícitamente a más de un parámetro desde el cuerpo de la solicitud, producirá una excepción. Cuerpo de la solicitud.
[FromForm] Inferido a partir de cualquier parámetro del tipo IFormFile o IFormFileCollection, por ejemplo, cuando hacemos envío de archivos como imágenes o documentos. Si el parámetro está contenido dentro de un tipo complejo definido por el usuario, no se realiza la inferencia, hay que indicar explícitamente la fuente de información. Datos del formulario en el cuerpo de la solicitud
[FromRoute] Inferido para cualquier parámetro de acción donde el nombre de la variable de parámetro coincida con un parámetro de ruta, es decir, aquellos que colocamos en la plantilla de ruta del endpoint o controlador. A través de parámetros de consulta en la URI.
[FromQuery] Inferido para cualquier otro parámetro de acción. Desde la ruta de la solicitud actual, es decir, parte de la URI.
[FromHeader] No hay una regla de inferencia para el enlace por encabezado. Encabezado de la solicitud.
[FromServices] Parámetros de tipo complejo que se encuentran registrados en el contenedor de dependencias. Servicio de solicitud extraído del contenedor de dependencias

Si nosotros no hacemos uso del atributo ApiControllerAttribute y además no colocamos ningún atributo de enlace de modelo para la fuente, podemos tener comportamientos inesperados, como por ejemplo, un objeto complejo se espere a través de QueryString en lugar del Body o Payload de la solicitud.

Podemos desactivar todas las reglas de inferencia agregando la configuración SuppressInferBindingSourcesForParameters a true dentro del comportamiento de nuestra API:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressInferBindingSourcesForParameters = true;
    });
Enter fullscreen mode Exit fullscreen mode

Podemos deshabilitar la inferencia de servicios para un único parámetro de acción, aplicando el atributo correspondiente al origen del parámetro, en lugar de usar FromServices (por ejemplo, FromBody).

Por defecto, ApiControllerAttribute aplica la inferencia para los parámetros de tipo IFormFile y IFormFileCollection, el tipo de contenido de la solicitud multipart/form-data se infiere para estos dos tipos, de tal forma que agrega por defecto una restricción de acción donde los datos deben ser enviados con el tipo de contenido multipart/form-data, de esta manera los parámetros enlazados deben ser consumidos a partir de los datos de formulario (FormData).

Si queremos deshabilitar este comportamiento por defecto podemos cambiar la propiedad SuppressConsumesConstraintForFormFileParameters a true.

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
    });
Enter fullscreen mode Exit fullscreen mode

Client Problem Details

ASP .NET Core realiza una transformación o mapeo a todos los resultados de error del cliente, es decir, cuando una se envía una respuesta con código de estado 400 o superior. Esta transformación por defecto se basa en la clase ProblemDetails que a su vez se basa en la misma especificación que ValidationProblemDetails, es decir, la RFC 7807 para respuestas de error con mayor detalle, es más, ValidationProblemDetails hereda de HttpValidationProblemDetails que a su vez hereda de ProblemDetails, por lo tanto, están dentro de la misma jerarquía de clases.

Un código de respuesta 404 Not Found se vería de la siguiente manera por defecto:

Imagen con código de estado 404 por defecto

El método NotFound() dentro del controlador genera el código de estado HTTP 404, donde el cuerpo será un ProblemDetails.

Si queremos deshabilitar la respuesta por defecto de ProblemDetails de forma local, es decir, para un solo endpoint podemos sobreescribirlo enviando la sobrecarga del parámetro value que recibe el método NotFound():

return NotFound($"La persona que estas buscando no ha sido encontrada.");
Enter fullscreen mode Exit fullscreen mode

La respuesta se vería de la siguiente manera:

Imagen con código de estado 404 pasando un valor

Pero si queremos deshabilitar la respuesta de ProblemDetails para todos los códigos de estado de error, podemos cambiar el valor a true de la propiedad SuppressMapClientErrors dentro de las opciones de comportamiento del API:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });
Enter fullscreen mode Exit fullscreen mode

Con esto, al ocurrir un error y devolverse un código de estado de error del cliente no tendremos un cuerpo de respuesta por defecto, ya que hemos eliminado el mapeo de errores del cliente:

Imagen con código de estado 404 con SuppressMapClientErrors en true

SuppressMapClientErrors por defecto es false, lo que hará que exista el mapeo, esto a su vez, agrega un filtro de resultado implícitamente a las acciones del controlador que transforman IClientErrorActionResult a una instancia de ProblemDetails que a su vez es devuelta como un ObjectResult que en su propiedad Value contiene una instancia de ProblemDetails.

Para personalizar la salida del filtro (por ejemplo, para devolver un tipo de error diferente), podemos registrar una implementación personalizada de IClientErrorFactory en la colección de servicios:

public sealed class AppClientErrorFactory(ILogger<AppClientErrorFactory> logger) : IClientErrorFactory
{
    private readonly ILogger<AppClientErrorFactory> _logger = logger;
    public IActionResult? GetClientError(ActionContext actionContext, IClientErrorActionResult clientError)
    {
        _logger.LogWarning("Error del cliente.");
        return new ObjectResult("ClientErrorFactory personalizado.") { StatusCode = clientError.StatusCode };
    }
}
Enter fullscreen mode Exit fullscreen mode

Como podemos ver es una clase que simplemente implementa la interfaz IClientErrorFactory. El método GetClientError recibe el ActionContext que ya vimos anteriormente y además una instancia de IClientErrorActionResult de la cual podemos conocer el código de estado de la respuesta o procesar el resultado para modificarlo. En este caso simplemente se mapea a un ObjectResult y se utiliza el servicio de logs para registrar algo que sea de nuestro interés.

En esta clase podemos inyectar cualquier servicio que necesitemos ya que va a ser parte de nuestro contenedor de dependencias, por lo tanto debemos registrarlo como servicio Singleton en nuestro contenedor para reemplazar la implementación por defecto:

builder.Services.AddSingleton<IClientErrorFactory, AppClientErrorFactory>();
Enter fullscreen mode Exit fullscreen mode

Respuesta con código de estado 404 de IClientErrorFactory

Ten en cuenta que si SuppressMapClientErrors es true, el IClientErrorFactory personalizado no será utilizado.

Conclusión

De esta manera hemos aprendido para qué sirve el atributo ApiControllerAttribute y como agregarlo a nuestros controladores de recursos para aplicar características y comportamientos predefinidos en ASP .NET Core. Además, hemos aplicado distintas configuraciones que permiten personalizar este comportamiento por defecto, como lo son la respuesta de validación de estado del modelo, códigos de error del cliente, restricciones sobre tipos de archivos, inferencia de tipos y más.

Código fuente

Recursos adicionales

https://code-maze.com/apicontroller-attribute-in-asp-net-core-web-api/

https://learn.microsoft.com/es-mx/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute

https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-9.0

https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api

Billboard image

Synthetic monitoring. Built for developers.

Join Vercel, Render, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay