En este segundo artículo, veremos como enriquecer el proyecto que teníamos, implementando documentación dentro de OpenApi, en este caso swagger, conectándonos a una base de datos con entityFramework y añadiendo autenticación a nuestro Api. Veremos en cada uno de los siguientes apartados como ir añadiendo als distintas funcionalidades a nuestro proyecto.
EntityFrameworkCore 6 y minimap API
Vamos a añadir EntityFramework para tener una base de datos y tener datos que explotar para poder hacer un CRUD completo. En este acaso, como es un proyecto para realizar pruebas, añadiremos EntityFramework inMemory, que nos permite de forma sencilla tener una base de datos para hacer unas pequeñas pruebas. Lo primero añadimos el nuget necesario:
Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 6.0.1
Una vez Instalado el paquete, generaremos dentro de nuestro Program.cs el DbContext necesario para un pequeño modelo de datos (particulas elementales).
ParticlesDB
[scss]class ParticlesDB : DbContext
{
public ParticlesDB(DbContextOptions options) : base(options) { }
public DbSet Particles => Set();
}[/scss]
Este DbContext es la representacion de un modelo de datos, que basicamente es:
Particles
[php]class Particle
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Symbol { get; set; }
public string? Spin { get; set; }
public string? Charge { get; set; }
public double Mass { get; set; }
public Type Type { get; set; }
public string? TypeName { get; set; }
}
enum Type
{
Quark,
Lepton,
Boson
}[/php]
Una vez añadido el contexto en la inicialización de los servicios.
[scss]builder.Services.AddDbContext(options =>
options.UseInMemoryDatabase(«Particles»));[/scss]
Ya tendríamos disponible el DbSet de Particles para operar sobre el.
Adicionalmente, como estamos en un entorno de pruebas, podríamos iniciar la base datos en el arranque de nuestro api, para ello, necesitamos una clase extra en la que deleguemos la población de la misma, y llamarla antes del arranque de la aplicación.
[scss]using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
DataGenerator.Initialize(scope.ServiceProvider);
app.Run();[/scss]
Documentación OpenApi
Ya teníamos el SwaggerUI disponible. Ahora vamos a modificar nuestros endpoint para tener un CRUD que ataque a nuestra base de datos recién creada. Podemos indicar mediante la extensión de
Microsoft.AspNetCore.Http, lo que aceptan y producen nuestros endpoints para que swagger nos muestre la documentación.
Nos quedarían los siguientes endpoints:
[scss]app.MapGet(«/particles»,
async (ParticlesDB db) =>
await db.Particles.ToListAsync()
)
.Produces(StatusCodes.Status200OK) .
WithName(«GetAllParticles»);
app.MapGet(«/particles/{id}»,
async (int id, ParticlesDB db) =>
await db.Particles.FirstOrDefaultAsync(p => p.Id == id) is Particle particle
?Results.Ok(particle)
:Results.NotFound()
)
.Produces(StatusCodes.Status200OK);
.Produces(StatusCodes.Status404NotFound)
.WithName(«GetParticleById»);
app.MapPost(«/particles»,
async (Particle particle, ParticlesDB db) =>
{
db.Particles.Add(particle);
await db.SaveChangesAsync();
return Results.Created($»/particles/{particle.Id}», particle);
})
.Accepts(«application/json»)
.Produces(StatusCodes.Status201Created)
.WithName(«CreateParticle»);
app.MapPut(«/particles»,
async (Particle particle, ParticlesDB db) =>
{
db.Particles.Add(particle);
await db.SaveChangesAsync();
return Results.Created($»/particles/{particle.Id}», particle);
})
.Accepts(«application/json»)
.Produces(StatusCodes.Status201Created)
.WithName(«CreateParticle»);
app.MapPut(«/particles»,
async (Particle particle, ParticlesDB db) =>
{
var entity = db.Particles.FirstOrDefaultAsync(p => p.Id == particle.Id).Result;
if (entity == null)
return Results.NotFound();
entity.Name = particle.Name;
entity.Symbol = particle.Symbol;
entity.Type = particle.Type;
entity.Mass = particle.Mass;
entity.Charge = particle.Charge;
entity.Spin = particle.Spin;
db.Particles.Update(entity);
await db.SaveChangesAsync();
return Results.Ok(entity);
})
.Accepts(«application/json»)
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.WithName(«UpdateParticle»);
app.MapDelete(«/particles/{id}»,
async (int id, ParticlesDB db) =>
{
var entity = db.Particles.FirstOrDefault(p => p.Id == id);
if (entity != null)
{
db.Particles.Remove(entity);
await db.SaveChangesAsync();
return Results.Ok(true);
}
return Results.Ok(false);
})
.Produces(StatusCodes.Status200OK);[/scss]
Y tendríamos la siguiente salida en el explorador.
Inyección de dependencias
Ahora para más claridad, podemos hacer uso de un servicio en el que vamos a delegar el manejo de nuestro contexto. Para ello es tan sencillo como crear una interfaz e implementar los métodos para la obtención, creación, actualización y borrado de nuestras entidades.
Después inyectaremos este servicio y se lo pasaremos como parámetro al mapeo de nuestras rutas.
[scss]builder.Services.AddScoped<IParticlesService, ParticlesService>();
// other code
app.MapGet(«/particles»,
async (IParticlesService srv) =>
await srv.GetAllParticles()
)
.Produces<List>(StatusCodes.Status200OK)
.WithName(«GetAllParticles»)
.RequireAuthorization();
// other code
class ParticlesService : IParticlesService
{
private readonly ParticlesDB _context;
public ParticlesService(ParticlesDB context)
{
_context = context;
}
public async Task AddParticle(Particle particle)
{
var entity = _context.Particles.Add(particle).Entity;
await _context.SaveChangesAsync();
return entity;
}
public async Task<IEnumerable> GetAllParticles()
{
return await _context.Particles.ToListAsync();
}
public async Task GetParticle(int id)
{
return await _context.Particles.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task RemoveParticle(int id)
{
return await _context.Particles.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task RemoveParticle(int id)
{
var entity = _context.Particles.FirstOrDefault(p => p.Id == id);
if (entity != null)
{
_context.Particles.Remove(entity);
await _context.SaveChangesAsync();
return true;
}
return false;
}
public async Task UpdateParticle(Particle particle)
{
var entity = _context.Particles.FirstOrDefaultAsync(p => p.Id ==
particle.Id).Result;
if (entity != null)
{
entity.Name = particle.Name;
entity.Symbol = particle.Symbol;
entity.Type = particle.Type;
entity.Mass = particle.Mass;
entity.Charge = particle.Charge;
entity.Spin = particle.Spin;
_context.Particles.Update(entity);
await _context.SaveChangesAsync();
}
return entity;
}
}
[/scss]
Es importante señalar que como dentro del Servicio, hacemos uso del DbContext, que está inyectado como Scope, el propio servicio, debe ser inyectado de igual manera.
Autenticación y Autorización
Por último, podemos añadir seguridad a nuestro api. Para ello haremos uso de un IdentityServer para generar nuestro token de acceso para un apiClient.
Creamos un proyecto de IdentityServer, el mas sencillo posible lo podemos crear gracias a las plantillas, de la siguiente manera: dotnet new isempty -n IdentityServer
Una vez que esté corriendo el servidor, podemos pedir un token de acceso con la configuración por defecto, con la llamada
[scss]curl –location –request POST ‘https://localhost:5001/connect/token’ \
–header ‘Content-Type: application/x-www-form-urlencoded’ \
–data-urlencode ‘client_id=client’ \
–data-urlencode ‘client_secret=secret’ \
–data-urlencode ‘scope=api1’ \
–data-urlencode ‘grant_type=client_credentials'[/scss]
Y recibiremos una respuesta del tipo:
[scss]{
«access_token»:
«eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ3OUZDRjVDNjFBQzI4NjMwMTdFR
Tc1RUVFN0EyOUMzIiwidHlwI
joiYXQrand0In0.eyJuYmYiOjE2Mzk5ODY5NDIsImV4cCI6MTYzOTk5M
DU0MiwiaXNzIjoiaHR0cHM6Ly9
sb2NhbGhvc3Q6NTAwMSIsImNsaWVudF9pZCI6ImNsaWVudCIsImp0a
SI6IjY3NTZDOERENjNFN0U1OTdDM
DJCRkVDQkY3NUYxNjRFIiwiaWF0IjoxNjM5OTg2OTQyLCJzY29wZSI6
WyJhcGkxIl19.HILRacQRZgoVNT
FoeuW9IejJIYxfaafyy23nBuL2Iv7bA2ri2OjpzViQpJC3SrlHwLN0nSL83c
KPsBDrZGhaMHkOg4eWR3j16k54UWx7W8eLO
y4TEYnJ6CRoBHDKemuUhDUkg0NW5gP18ix4ynE1fYjA4V3SMtFr42g
KJBh5sHMkVmBPpwYH7ZVFVyq8gDmR5peFZYd69N8jrRM6qA7_lrEe
HkKXXi0BppJkAuZd9UjgyEHjaNBVHn6afQqZGNegSlEi3Tn1
2sLZGh3DFFVyeDgYEwQPhJILjqZMYnzmog4jSMn-NLfrTfUU_Fd9Hxn
UmbvhXDyb0jkT0xR70H4w»,
«expires_in»: 3600,
«token_type»: «Bearer»,
«scope»: «api1»
}
[/scss]
Una vez que tengamos esta parte funcionando, vamos a enriquecer nuestro API para añadirle seguridad, lo primero, deberemos instalar el nuget Microsoft.AspNetCore.Authentication.JwtBearer y añadir lo siguiente a nuestro Program:\
[scss]//Services
builder.Services.AddAuthentication(«Bearer»)
.AddJwtBearer(«Bearer», options =>
{
options.Authority = «https://localhost:5001»;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(«ApiScope», policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(«scope», «api1»);
});
});
…
//Middleware
app.UseAuthentication();
app.UseAuthorization();[/scss]
Ahora podemos indicar a nuestros endpoint que estan publicados bajo una capa de seguridad, y necesitan de autenticacion para ser llamados de cualquiera de las formas siguientes
[scss]app.MapGet(«/particles»,
async (IParticlesService srv) =>
await srv.GetAllParticles()
)
.Produces<List>(StatusCodes.Status200OK)
.WithName(«GetAllParticles»)
.RequireAuthorization();[/scss]
O bien
[scss]app.MapGet(«/particles»,
[Authorize] async (IParticlesService srv) =>
await srv.GetAllParticles()
)
.Produces<List>(StatusCodes.Status200OK)
.WithName(«GetAllParticles»);[/scss]
De esta forma, si hacemos la petición a nuestro endpoint, recibiremos un 401
Para ver que todo funciona, por último hacemos una llamada a nuestro servicio, añadiendo el token de autorización para comprobar que contesta correctamente «`console curl –location –request GET ‘https://localhost:7113/particles’ \ –header ‘Authorization: Bearer {access_token}’ «` Obteniendo una respuesta correcta:
Si quieres ver o descargar el código correspondiente a este artículo, está disponible en el siguiente enlace minimal-apis-sample.
Siente libre de hacer cualquier aportación al mismo.