Imaginad que tenéis un sistema CMS con una WebAPI donde vuestros clientes pueden guardar y ver sus artículo en la base de datos. Como buenos programadores, queréis proteger vuestro sistema CMS y a la vez aportarle la máxima información útil al cliente.
[HttpGet("{id}")]
public ArticuloGetDTO GetById(int id)
{
if (id <= 0) return null;
Articulo articulo = _articuloRepository.GetArticuloById(id);
if(articulo == null) return null;
return new ArticuloGetDTO()
{
Id = articulo.Id,
Titulo = articulo.Titulo,
Contenido = articulo.Contenido,
FechaCreacion = articulo.FechaCreacion
};
}
En este caso técnicamente podemos devolvero diferentes tipos, por un lado ArticuloGetDTO y null. Estamos validando adecuadamente pero el cliente jamás se entera de qué ha hecho mal. Si el cliente intenta acceder al artículo con Id «0», no obtiene nada como respuesta, como podeis ver en la siguiente imagen:

Pero claro, si ahora decimos que en esos «return» devuelvan un mensaje, entonces el tipo de retorno estaría obligado a ser «string» y no podríamos devolver ArticuloGetDTO… ¿Qué podemos hacer en estos casos?
Devolver diferentes tipos con la clase Resultado
La clase Resultado que vamos a implementar tienen como objetivo envolver el resultado de la operación y el objeto que se debe obtener como resultado de esa transacción. Esto nos permite devolver diferentes tipos, en tanto y en cuanto compongan a la clase Resultado.
En una primera instancia podríamos mantener los resultados simples, es decir, ocurre o no ocurre la transacción, y en caso de no ocurrir, por qué.
public class Resultado
{
public bool EsValido { get; }
public string Errores { get; }
protected Resultado(bool esValido, string error)
{
// Si es correcto no deberían haber errores
if (esValido && error != string.Empty)
throw new InvalidOperationException();
// Si es incorrecto tienen que haber errores
if (!esValido && error == string.Empty)
throw new InvalidOperationException();
EsValido = esValido;
Errores = error;
}
public static Resultado Fail(string mensaje)
=> new Resultado(false, mensaje);
public static Resultado<T> Fail<T>(string mensaje)
=> new Resultado<T>(default(T), false, mensaje);
public static Resultado Ok()
=> new Resultado(true, string.Empty);
public static Resultado<T> Ok<T>(T value)
=> new Resultado<T>(value, true, string.Empty);
}
public class Resultado<T> : Resultado
{
private readonly T _entidad;
public T Entidad
{
get
{
// Si no es válido, no debe llevar un objeto de la clase T
if (!EsValido) throw new InvalidOperationException();
return _entidad;
}
}
protected internal Resultado(T entidad, bool esValido, string error) : base(esValido, error)
{
_entidad = entidad;
}
}
Ahora podemos dar información muchísimo más útil al cliente.
[HttpGet("{id}")]
public Resultado GetById(int id)
{
if (id <= 0)
return Resultado.Fail("Id no puede ser menor a 0.");
Articulo articulo = _articuloRepository.GetArticuloById(id);
if(articulo == null)
return Resultado.Fail("No se ha encontrado artículo con ese Id."); ;
return Resultado.Ok(new ArticuloGetDTO()
{
Id = articulo.Id,
Titulo = articulo.Titulo,
Contenido = articulo.Contenido,
FechaCreacion = articulo.FechaCreacion
});
}
Por lo que, ante la misma situación del inicio, el cliente ahora ve lo siguiente:

Sin duda con esto ya es más fácil trabajar, ahora el cliente sabe qué ha pasado y puede modificar su consulta apropiadamente. Por supuesto, esto es mejorable utilizando el patrón State o incluyendo diferentes tipos de validaciones que sepamos que vamos a utilizar en un Enum y que sea un parámetro extra del Resultado.
public enum ResultadoError
{
ParametroNulo,
StringVacio,
ObjetoNoEncontrado
}
public class Resultado
{
public Tuple<ResultadoError, string> Errores { get; }
/* MODIFICACIONES APROPIADAS*/
}
Esto puede potencialmente mejorar la testabilidad del código. Esto se debe a que los mensajes son un string que potencialmente va a cambiar con el tiempo o puede ponerse en varios idiomas, mientras que los valores del enum reflejan la tipologia del error, que es mucho menos probable que cambie. Comparad el código:
[Fact]
public void InsertarArticulo_ArticuloNulo_DevuelveMensajeError()
{
// Por simplicidad, voy a llamar al método directamente
Resultado resultado = InsertarArticulo(null);
// Testeando sobre el mensaje
Assert.Equal("Id no puede ser menor a 0.", resultado.Errores);
// Testeando sobre el enum
Assert.Equal(ResultadoError.ParametroNulo, resultado.Errores.Item1);
}
¿Cuál creeis que es más factible que cambie? Es más posible que yo decida quitarle el punto final del mensaje, a que deje de considerar que esa validación se trata de un ResultadoError.ParametroNulo. Por esta razón, es una forma conveniente de mejorar la implementación.
De hecho, los controladores WebAPI de .NET utilizan un sistema similar usando IActionResult como valor de retorno. Por lo que lo que hemos hecho podría traducirse de la siguiente forma:
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
if (id <= 0)
return BadRequest("Id no puede ser menor a 0.");
Articulo articulo = _articuloRepository.GetArticuloById(id);
if(articulo == null)
return NotFound("No se ha encontrado artículo con ese Id.");
return Ok(new ArticuloGetDTO()
{
Id = articulo.Id,
Titulo = articulo.Titulo,
Contenido = articulo.Contenido,
FechaCreacion = articulo.FechaCreacion,
AutorId = articulo.Autor.Id,
Nombre = articulo.Autor.Nombre.Trim(),
Apellidos = articulo.Autor.Apellido.Trim()
});
}
Que sería una forma más apropiada de no reinventar la rueda y reutilizar lo que el framework te ofrece. Sin embargo, este concepto reutiliza esa idea y está más orientado a métodos que no son obligatoriamente controladores y no requieren de una respuesta HTTP.
Referencias
- Khorikov, Vladimir (2015) «Functional C#: Handling failures, input errors».
- Microsoft Docs (2022) «Controller action return types in ASP.NET Core web API».