Cómo crear un cliente C# para un Web API de ASP.NET Core (I)
NSwag Studio es una aplicación de escritorio de Windows, que nos permite generar código de cliente C# o TypeScript para Web APIs de ASP.NET, siempre y cuando estos expongan su especificación OpenAPI/Swagger del tipo swagger.json
o equivalente.
La ventaja que nos proporciona la auto-generación de código cliente con NSwag Studio, es básicamente la posibilidad de acelerar el ciclo de desarrollo de nuestras aplicaciones, adaptándonos fácilmente a los cambios que puedan experimentar los Web APIs.
En este artículo veremos cómo generar un cliente C# (librería dll) para un Web API de ASP.NET Core, que posteriormente utilizaremos para consumir dicho Web API desde una aplicación ASP.NET Core MVC.
Integrando Swagger en nuestro Web API
Para realizar este ejemplo, en primer lugar reutilizaremos el proyecto Web API RESTful de .NET Core que ya desarrollamos en el artículo de este blog JSON Web Token - Seguridad en servicios Web API de .NET Core.
A continuación, integraremos Swagger en el proyecto, según las especificaciones que ya vimos en el artículo de este blog Swagger - Cómo documentar servicios Web API de ASP.NET Core.
El atributo [ProducesResponseType(...)]
En este punto del desarrollo, ya podríamos utilizar la herramienta NSwag Studio para generar el cliente C# para nuestro Web API, pero antes debemos realizar una serie de configuraciones previas, que nos aseguren que el cliente auto-generado quedará libre de errores en lo que se refiere a los resultados devueltos esperados.
NSwag utiliza Reflection para determinar el valor devuelto por las Acciones de nuestro Web API. En los casos en los que una Acción devuelva un ActionResult
o un IActionResult
sin indicar el tipo (IActionResult<T> o ActionResult<T>
) NSwag no tendrá forma alguna de resolver el tipo (typeof) devuelto por la Acción.
Es por esto que debemos indicarlo nosotros de forma explícita mediante el atributo [ProducesResponseType(...)]
indicando el tipo de la respuesta (typeof(...)
) y el código de estado Http (StatusCode
).
Importante: Como ya dijimos, esta es una forma de asegurarnos que las respuestas de las Acciones de nuestro Web API serán correctamente interpretadas por NSwag Studio, así que es altamente recomendable incluir siempre en las Acciones de nuestros Web API el atributo
[ProducesResponseType(typeof(...), StatusCodes.Status...)]
para indicar el tipo (typeof) de la respuesta predeterminada esperada.
Los Controladores de Web API
Una vez aplicadas estas recomendaciones, los Controladores de nuestro Web API quedarían de la siguiente forma:
Para el Controlador LoginController.cs
el código sería el siguiente:
[SwaggerTag("Login",
Description = "Web API para autenticación de usuarios.",
DocumentationDescription = "Documentación externa",
DocumentationUrl = "http://rafaelacosta.net/login-doc.pdf")]
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class LoginController : ControllerBase
{
private readonly IConfiguration configuration;
// TRAEMOS EL OBJETO DE CONFIGURACIÓN (appsettings.json)
// MEDIANTE INYECCIÓN DE DEPENDENCIAS.
public LoginController(IConfiguration configuration)
{
this.configuration = configuration;
}
// POST: api/Login
/// <summary>
/// Autentica a un usuario en el sistema, y devuelve un Token JWT de acceso.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario.
/// </remarks>
/// <param name="usuarioLogin">Objeto con las credenciales de acceso del usuario</param>
/// <response code="200">OK. Devuelve un objeto anónimo con el Token JWT de acceso.</response>
/// <response code="401">Unauthorized. las credenciales de acceso del usuario son incorrectas.</response>
[HttpPost]
[AllowAnonymous]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[SwaggerResponse(typeof(object))]
public async Task<IActionResult> Login(UsuarioLogin usuarioLogin)
{
var _userInfo = await AutenticarUsuarioAsync(usuarioLogin.Usuario, usuarioLogin.Password);
if (_userInfo != null)
{
return Ok(new { token = GenerarTokenJWT(_userInfo) });
}
else
{
return Unauthorized();
}
}
// COMPROBAMOS SI EL USUARIO EXISTE EN LA BASE DE DATOS
private async Task<UsuarioInfo> AutenticarUsuarioAsync(string usuario, string password)
{
// AQUÍ LA LÓGICA DE AUTENTICACIÓN //
// Supondremos que el Usuario existe en la Base de Datos.
// Retornamos un objeto del tipo UsuarioInfo, con toda
// la información del usuario necesaria para el Token.
return new UsuarioInfo()
{
// Id del Usuario en el Sistema de Información (BD)
Id = new Guid("B5D233F0-6EC2-4950-8CD7-F44D16EC878F"),
Nombre = "Nombre Usuario",
Apellidos = "Apellidos Usuario",
Email = "email.usuario@dominio.com",
Rol = "Administrador"
};
// Supondremos que el Usuario NO existe en la Base de Datos.
// Retornamos NULL.
//return null;
}
// GENERAMOS EL TOKEN CON LA INFORMACIÓN DEL USUARIO
private string GenerarTokenJWT(UsuarioInfo usuarioInfo)
{
// CREAMOS EL HEADER //
var _symmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["JWT:ClaveSecreta"])
);
var _signingCredentials = new SigningCredentials(
_symmetricSecurityKey, SecurityAlgorithms.HmacSha256
);
var _Header = new JwtHeader(_signingCredentials);
// CREAMOS LOS CLAIMS //
var _Claims = new[] {
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.NameId, usuarioInfo.Id.ToString()),
new Claim("nombre", usuarioInfo.Nombre),
new Claim("apellidos", usuarioInfo.Apellidos),
new Claim(JwtRegisteredClaimNames.Email, usuarioInfo.Email),
new Claim(ClaimTypes.Role, usuarioInfo.Rol)
};
// CREAMOS EL PAYLOAD //
var _Payload = new JwtPayload(
issuer: configuration["JWT:Issuer"],
audience: configuration["JWT:Audience"],
claims: _Claims,
notBefore: DateTime.Now,
// Exipra a la 24 horas.
expires: DateTime.Now.AddHours(24)
);
// GENERAMOS EL TOKEN //
var _Token = new JwtSecurityToken(
_Header,
_Payload
);
return new JwtSecurityTokenHandler().WriteToken(_Token);
}
}
Si observamos el código, la Acción Login()
del Controlador LoginController
es un claro ejemplo de la necesidad de utilizar el atributo [ProducesResponseType(...)]
.
Como podemos ver, la Acción Login()
devuelve un Task<IActionResult>
, pero realmente lo que esperamos obtener es un objeto anónimo con el Token JWT de acceso solicitado: return Ok (new { token = GenerarTokenJWT(_userInfo) })
.
En estos casos NSwag no sabe como interpretar o enlazar el objeto anónimo a devolver con el tipo IActionResult
, por lo que posiblemente al generar el cliente C#, nos devuelva un respuesta vacía o incorrecta.
Esto lo solucionamos añadiendo la etiqueta [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
a la Acción Login()
, ya que object
es el tipo devuelto esperado.
En lo que respecta al Controlador PaisController.cs
, este sería el resultado:
[SwaggerTag("Pais",
Description = "Web API para mantenimiento de Países.",
DocumentationDescription = "Documentación externa",
DocumentationUrl = "http://rafaelacosta.net/pais-doc.pdf")]
[Route("api/[controller]")]
[ApiController]
public class PaisController : ControllerBase
{
private readonly ApplicationDbContext _context;
public PaisController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/Pais
/// <summary>
/// Obtiene los objetos de la base de datos.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario. Obtiene todos los objetos de la base de datos.
/// </remarks>
/// <response code="200">OK. Devuelve una lista con los objetos de la base de datos.</response>
/// <response code="401">Unauthorized. No se ha indicado o es incorrecto el Token JWT de acceso.</response>
[HttpGet]
[Authorize]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(IEnumerable<Pais>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<IEnumerable<Pais>>> GetPais()
{
return await _context.Pais.ToListAsync();
}
// GET: api/Pais/5
/// <summary>
/// Obtiene un objeto por su Id.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario. Obtiene un objeto por su Id.
/// </remarks>
/// <param name="id">Id (GUID) del objeto.</param>
/// <response code="401">Unauthorized. No se ha indicado o es incorrecto el Token JWT de acceso.</response>
/// <response code="200">OK. Devuelve el objeto solicitado.</response>
/// <response code="404">NotFound. No se ha encontrado el objeto solicitado.</response>
[HttpGet("{id}")]
[Authorize]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(Pais), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Pais>> GetPais(Guid id)
{
var pais = await _context.Pais.FindAsync(id);
if (pais == null)
{
return NotFound();
}
return pais;
}
// POST: api/Pais
/// <summary>
/// Crea un nuevo objeto en la BD.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario. Crea un nuevo objeto en la BD.
/// </remarks>
/// <param name="pais">Objeto a crear a la BD.</param>
/// <response code="401">Unauthorized. No se ha indicado o es incorrecto el Token JWT de acceso.</response>
/// <response code="201">Created. Objeto correctamente creado en la BD.</response>
/// <response code="400">BadRequest. No se ha creado el objeto en la BD. Formato del objeto incorrecto.</response>
[HttpPost]
[Authorize]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(Pais), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Pais>> PostPais(Pais pais)
{
_context.Pais.Add(pais);
await _context.SaveChangesAsync();
return CreatedAtAction("GetPais", new { id = pais.Id }, pais);
}
// PUT: api/Pais/5
/// <summary>
/// Modifica un objeto existente en la BD.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario. Actualiza un objeto existente en la BD.
/// </remarks>
/// <param name="id">Id del objeto a modificar en la BD.</param>
/// <param name="pais">Objeto con las modificaciones a realizar.</param>
/// <response code="204">NoContent. Objeto correctamente modificado en la BD.</response>
/// <response code="401">Unauthorized. No se ha indicado o es incorrecto el Token JWT de acceso.</response>
/// <response code="400">BadRequest. el Id del objeto a modificar no coincide con el parámetro Id.</response>
/// <response code="404">NotFound. No se ha encontrado el objeto a modificar en la BD.</response>
[HttpPut("{id}")]
[Authorize]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> PutPais(Guid id, Pais pais)
{
if (id != pais.Id)
{
return BadRequest();
}
_context.Entry(pais).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!PaisExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// DELETE: api/Pais/5
/// <summary>
/// Elimina un objeto de la BD.
/// </summary>
/// <remarks>
/// Aquí una descripción mas larga si fuera necesario. Elimina un objeto de la BD.
/// </remarks>
/// <param name="id">Id del objeto a eliminar de la BD.</param>
/// <response code="200">OK. Devuelve el objeto eliminado.</response>
/// <response code="401">Unauthorized. No se ha indicado o es incorrecto el Token JWT de acceso.</response>
/// <response code="404">NotFound. No se ha encontrado el objeto a eliminar en la BD.</response>
[HttpDelete("{id}")]
[Authorize]
/// RESPUESTA PREDETERMINADA ESPERADA ///
[ProducesResponseType(typeof(Pais), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Pais>> DeletePais(Guid id)
{
var pais = await _context.Pais.FindAsync(id);
if (pais == null)
{
return NotFound();
}
_context.Pais.Remove(pais);
await _context.SaveChangesAsync();
return pais;
}
private bool PaisExists(Guid id)
{
return _context.Pais.Any(e => e.Id == id);
}
}
Como podemos ver en el código, hemos indicado el tipo (typeof) de la respuesta predeterminada esperada, en todas las Acciones del Controlador mediante el atributo [ProducesResponseType(typeof(...), StatusCodes.Status...)]
.
Como comentamos anteriormente, esta práctica es altamente recomendada para que NSwag Studio genere correctamente el cliente C#, aun así y en este ejemplo, la única Acción que requiere obligatoriamente que indiquemos su tipo de respuesta esperada, es la Acción PUT public async Task<IActionResult> PutPais(Guid id, Pais pais)
.
Si observamos, la Acción PUT devuelve un Task<IActionResult>
, pero lo que realmente esperamos es una respuesta del tipo NoContent()
(Status 204 con contenido vacío). Es por esto que debemos indicar explícitamente el tipo de resultado esperado mediante el atributo [ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
, ya que void
es el tipo devuelto esperado.
Generando el cliente C# con NSwag Studio
En primer lugar, descargaremos el instalador (.msi) de NSwag Studio desde el repositorio GitHub del desarrollador.
Una vez instalada la herramienta, pasaremos a realizar las configuraciones previas. En el panel izquierdo de la ventana (Input), seleccionaremos el Runtime a utilizar, en nuestro caso utilizaremos NetCore2.2.
A continuación seleccionaremos la pestaña OpenAPI/Swagger Specification, e introduciremos la URL del archivo de especificación OpenAPI/Swagger de nuestro Web API. Seguidamente haremos click en el botón [Create local copy] para importar una copia de la especificación en formato JSON.
Una vez generada la copia local de la especificación OpenAPI/Swagger, pasaremos al panel derecho de la ventana (Outputs).
En primer lugar marcaremos el check CSharp Client y seleccionaremos su pestaña. Seguidamente seleccionaremos la pestaña Settings, donde podremos configurar múltiples características del cliente C# que posteriormente generaremos.
En principio, la configuración que viene por defecto es perfectamente válida para crear un cliente C# estándar, aun así, nosotros indicaremos el espacio de nombres que tendrá el cliente auto-generado, y nos aseguraremos de que estén marcadas las opciones de Generar Interfaces para las clases de Cliente e Inyectar HttpClient vía constructor.
Nota: la razón por la cual necesitamos generar interfaces para las clases de cliente e inyectar el HttpClient vía constructor, lo veremos más adelante cuando integremos este cliente C# auto-generado (librería dll) en una aplicación ASP.NET Core MVC y utilicemos la inyección de dependencias nativa de .NET Core.
Por último, haremos click en el botón [Generete Outputs], y si todo ha ido bien, ya tendremos generado el código C# completo para consumir nuestro Web API.
Creando la biblioteca de clases (dll)
Una vez obtenido el código C# para el cliente de nuestro Web API, el siguiente paso será crear una librería dll (biblioteca de clases), que nos permita "empaquetar" toda la funcionalidad auto-generada, para poder ser reutilizada posteriormente en cualquier aplicación que la requiera.
Es por esto que comenzaremos creando un nuevo proyecto en Visual Studio (WebApiClient.csproj
) del tipo Biblioteca de clases (.NET Standard).
Nota: La razón por la cual utilizamos una biblioteca .NET Standard, radica en poder mantener la compatibilidad de nuestro cliente de Web API, a la hora de poder ser integrado tanto en aplicaciones .NET Core como en aplicaciones .NET Framework.
A continuación crearemos en el proyecto un nuevo archivo de clases llamado NSwagClientCode.cs
. En este archivo copiaremos todo el código auto-generado anteriormente por la herramienta NSwag Studio.
Nota: El archivo con el código auto-generado por la herramienta NSwag Studio, está disponible para descargar al final de este artículo.
Debemos tener en cuenta que el archivo NSwagClientCode.cs
siempre contendrá el código auto-generado por NSwag Studio sin ninguna modificación adicional por nuestra parte.
Esto es así, porque el objetivo de auto-generar código es la posibilidad de acelerar el ciclo de desarrollo de nuestras aplicaciones, además de adaptarnos fácilmente a los cambios que puedan experimentar los Web APIs. En otras palabras, si cambia el Web API, se auto-genera un cliente nuevo y se copia en la clase NSwagClientCode.cs
.
Por supuesto, siempre necesitaremos realizar cambios o añadir funcionalidades al cliente auto-generado, pero eso, veremos a continuación cómo solucionarlo sin necesidad de "tocar" el código del cliente C# auto-generado.
La clases y métodos parciales (partial)
Si observamos el código del cliente generado por NSwag Studio, nos daremos cuenta que tanto las clases como las interfaces y métodos, están definidos como parciales (partial).
Como ya sabemos, una clase parcial es aquella que podemos definirla en mas de un archivo de código fuente, y es precisamente esta funcionalidad la que nos va a permitir realizar las modificaciones y ampliaciones a nuestro cliente C#, utilizando otro archivo de código fuente.
A la hora de generar el cliente C#, es importante tener en cuenta que NSwag crea en las clases principales una serie de métodos parciales "vacíos" para gestionar las peticiones (request) y las respuestas (response) que inrteractúan con el Web API.
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
El objetivo de estos métodos parciales, es el de permitir ampliar las funcionalidades de nuestro cliente C#, utilizándolos en clases parciales definidas por nosotros en archivos de código fuente diferentes.
Integrando el token JWT de acceso
Como ya vimos, el Web API que hemos utilizado para este ejemplo utiliza Json Web Token (JWT) como sistema de autenticación de usuarios. Por alguna razón, NSwag Studio no tiene en cuenta la autenticación de usuarios mediante tokens de acceso a la hora de generar el cliente C#, así que seremos nosotros los que debemos añadir esta funcionalidad manualmente.
Continuando con nuestro proyecto, ahora crearemos un nuevo archivo de clases llamado PaisClient.Extension.cs
. En este archivo definiremos todas las modificaciones y ampliaciones que necesitemos añadir a la clase parcial PaisClient()
, que ya se encuentra definida en el archivo NSwagClientCode.cs
.
Seguidamente añadiremos la funcionalidad JWT a la clase PaisClient()
, a través del método parcial PrepareRequest(...)
. Simplemente añadiremos el Token JWT a la cabecera de las peticiones, obteniéndolo a través de la propiedad pública public string BearerTokenJWT { get; set; }
.
El código fuente del archivo PaisClient.Extension.cs
para la clase PaisClient()
quedaría así:
// CREAMOS LA INTERFAZ PARCIAL DE LA CLASE PaisClient.
public partial interface IPaisClient
{
/// <summary>
/// Token JWT de acceso. Asignar siempre el Token JWT antes de la llamada a un método.
/// </summary>
string BearerTokenJWT { get; set; }
}
// CRFEAMOS LA CLASE PARCIAL PaisClient, DONDE IMPLEMENTAREMOS
// LAS NUEVAS FUNCIONALIDADES A AÑADIR A LA CLASE.
public partial class PaisClient : IPaisClient
{
/// <summary>
/// Token JWT de acceso. Asignar siempre el Token JWT antes de la llamada a un método.
/// </summary>
public string BearerTokenJWT { get; set; }
// SOBREESCRIBIMOS EL MÉTODO PARCIAL PrepareRequest, DONDE ASIGNAREMOS
// A LA CABECERA DE LAS PETICIONES EL TOKEN JWT.
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", BearerTokenJWT);
}
}
Por último, solo quedaría compilar el proyecto para generar la librería WebApiClient.dll
, que posteriormente podremos integrar en cualquier aplicación ASP.NET.
Continuará ...
Como ya hemos visto, generar un cliente C# para consumir un Web API mediante su especificación OpenAPI/Swagger, es bastante sencillo si utilizamos NSwag Studio.
Lógicamente y para complementar este artículo, el siguiente paso sería ver cómo integrar este cliente (librería dll) en una aplicación ASP.NET que requiera compartir información con nuestro Web API.
En el próximo artículo de este Blog Cómo crear un cliente C# para un Web API de ASP.NET Core (II), veremos cómo desarrollar una aplicación ASP.NET Core MVC que utilice la librería WebApiClient.dll
para consumir un Web API, teniendo en cuenta conceptos como la inyección de dependencias del HttpClient
, el almacenamiento y recuperación del Token JWT, etc.
Nuevo comentario
Comentarios
@Jose:
Efectivamente tienes razón. El archivo
PaisClient.Extension.cs
es el encargado de extender la clase parcialPaisClient
y también su interfazIPaisClient
.Aunque como está funciona perfectamente, la clase
PaisClient
debería implementar la interfazIPaisClient
dentro del mismo archivo:public partial class PaisClient : IPaisClient { ... }
.Ya lo he corregido en el artículo, Gracias por tu aportación!.
Buenas tardes, en la clase parcial PaisClient se implementa la interfaz IPaisClient ? ya que en la imagen no veo que lo implemente
La interfaz IPaisClient seria en un nuevo archivo?