saltar al contenido
Cómo crear una API web basada en roles con ASP.NET Core

Cómo crear una API web basada en roles con ASP.NET Core

Aquí hay otra excelente publicación de blog instructiva. Le mostrará cómo usar ASP.NET Core para crear una API web basada en roles y una interfaz de usuario Swagger para visualizar e interactuar con los puntos finales.

20 minutos de lectura

Cuando se trata de crear una API web basada en roles con ASP.NET Core, el enfoque de código primero puede ser un método potente y eficiente. Al usarlo, podemos definir nuestros modelos de datos y relaciones en código y luego generar el esquema de base de datos correspondiente automáticamente. ¿A qué conduce esto? Ciclos de desarrollo más rápidos y mayor flexibilidad sin duda. ¿Cómo? Porque los cambios en el modelo de datos se pueden realizar de forma rápida y sencilla, sin tener que modificar el esquema de la base de datos directamente. Puede leer más sobre los enfoques Design First y Code First en swagger.io.

Entonces, en este tutorial, cubriremos los pasos para crear una API web basada en roles usando ASP.NET Core 6. Usaremos la interfaz de usuario de Swagger para visualizar e interactuar con nuestros puntos finales y MS SQL Server como nuestra base de datos. La aplicación incluirá un módulo de autenticación y un módulo de eventos. Los usuarios que hayan iniciado sesión podrán ver los eventos asociados con su cuenta, mientras que los usuarios con el rol de Administrador podrán crear, actualizar y eliminar eventos.

¡Empecemos!

Configuración del proyecto

Primero, necesitamos configurar nuestro proyecto. Para hacerlo, abra Visual Studio, vaya a crear un nuevo proyecto y luego elija ASP.NET Core Web API.

Abra Visual Studio, vaya a crear un nuevo proyecto y luego elija ASP.NET Core Web API

Elija el nombre de la aplicación y haga clic en Siguiente.

Elija el nombre de la aplicación en Visual Studio

Configurar la base de datos API

Después de haber inicializado nuestra aplicación, necesitamos configurar la base de datos. Usaremos EntityFrameworkCore como ORM por lo que nos ayudará a administrar nuestra base de datos. Por este motivo, deberíamos instalar algunos paquetes.

Configurar la base de datos API en Visual Studio

Lo siguiente que debemos hacer después de haber instalado correctamente los paquetes es crear un DbContext. Cree el archivo DataContext.cs y herede la clase DBContext. Aquí vamos a definir nuestras tablas.

public class DataContext: DbContext
{
  public DataContext(DbContextOptions options): base(options)
  {
  }

  //Define our tables
}

Luego deberíamos abrir el archivo Program.cs y agregar el dbContext. Debemos especificar dbProvider y la cadena de conexión que provienen del archivo appsettings.json. El dbProvider puede ser SqlServer, MySql o InMemory.

// Add Db context

var dbProvider = builder.Configuration.GetConnectionString("Provider");

builder.Services.AddDbContext < DataContext > (options =>
  {

    if (dbProvider == "SqlServer")
    {
      options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnectionString"));
    }
  });

Asegúrese de haber agregado ConnectionString y Provider en su archivo appsettings.json de la siguiente manera:

…
"ConnectionStrings": {
  "Provider": "SqlServer",
  "SqlServerConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Database=HRApplication2;Integrated Security=True;Connect Timeout=30; "
},
…

Después de configurar DbContext, es necesario generar los modelos de base de datos. En este caso, necesitamos dos entidades (Usuario y Evento) y una tercera tabla (UserEvent) para establecer una relación de muchos a muchos entre ellas. Para lograr esto, se recomienda que creemos una carpeta Modelos y una subcarpeta DbModels dentro de ella donde podamos crear nuestras entidades de base de datos.

Comencemos con el modelo de usuario. Cada usuario debe tener una identificación única, correo electrónico, nombre, apellido, contraseña que se almacenará en formato hash, rol que puede ser usuario y administrador para la demostración y eventos de usuario que estarán relacionados con la tabla UserEvent.

public class User
{
    public string UserId { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
    public IList <UserEvent> UserEvents { get; set; }
}

El modelo de evento también debe tener un ID, título, categoría, fecha y también una relación únicos con la tabla UserEvents.

public class Event
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Category { get; set; }
    public DateTime Date { get; set; }
    public IList <UserEvent> UserEvents { get; set; }
}

Dado que un usuario puede asistir a varios eventos y varios usuarios pueden asistir a un evento, debemos establecer una relación de muchos a muchos entre estas entidades. Para hacer esto, crearemos una tabla adicional llamada UserEvents. Esta tabla incluirá columnas UserId y EventId que establecerán la relación entre las entidades Usuario y Evento.

public class UserEvent
{
    public string UserId { get; set; }
    public User User { get; set; }
    public string EventId { get; set; }
    public Event Event { get; set; }
}

Una vez que hemos creado nuestros modelos de base de datos, el siguiente paso es registrarlos en nuestro DbContext. Para lograr esto, podemos navegar hasta el archivo DataContext.cs, agregar todas las entidades como DbSets y declarar nuestras relaciones y claves principales. Esto se puede lograr anulando el método OnModelCreating y utilizando la API Fluent para configurar las relaciones y las claves. Una vez completado, el resultado debería aparecer de la siguiente manera:

public class DataContext: DbContext
{
  public DataContext(DbContextOptions options): base(options)
  {
  }
  
  public DbSet <User> Users { get; set; }
  public DbSet < Event > Events { get; set; }
  public DbSet < UserEvent > UserEvents { get; set; }

  protected override void OnModelCreating(ModelBuilder builder)
  {
    base.OnModelCreating(builder);

    builder.Entity<User>()
      .HasKey(u => new {
        u.UserId
      });

    builder.Entity<Event>()
      .HasKey(e => new {
        e.Id
      });

    builder.Entity<UserEvent>()
      .HasKey(ue => new {
        ue.UserId, ue.EventId
      });

    builder.Entity<UserEvent>()
      .HasOne(ue => ue.User)
      .WithMany(user => user.UserEvents)
      .HasForeignKey(u => u.UserId);

    builder.Entity<UserEvent>()
      .HasOne(uc => uc.Event)
      .WithMany(ev => ev.UserEvents)
      .HasForeignKey(ev => ev.EventId);
  }
}

Una vez que estemos listos con el diseño de la base de datos, debemos generar una migración inicial que creará la base de datos.

Abra la Consola del Administrador de paquetes y escriba el comando:

Agregar-Migración InicialCreate

Agregar migración inicialCreate en la consola del administrador de proyectos

Después de que se ejecute correctamente, debemos actualizar la base de datos con:

Actualizar base de datos

Luego, con Microsoft SQL Management Studio, debería ver la base de datos recién creada.

Configurando AutoMapper

AutoMapper nos ayudará a transformar un modelo en otro. Esto convertirá los modelos de entrada en dbModels. La razón por la que hacemos esto es que es posible que no necesitemos que todas las propiedades de uno de los modelos se incluyan en el otro modelo. Verás cómo exactamente lo usaremos más adelante en el tutorial. Antes de eso, primero debemos configurarlo. Puede encontrar una explicación más detallada de AutoMapper en la documentación oficial.

Para comenzar, debemos instalar el paquete AutoMaper NuGet. Después de esto, podemos generar un archivo MappingProfiles.cs para definir todas las asignaciones. Se recomienda crear este archivo en una carpeta de Ayudantes para fines de organización.

Para declarar nuestras asignaciones, MappingProfiles debe heredar la clase Profile y podemos declarar nuestras asignaciones usando el método CreateMap<from, to>(). Si requerimos la capacidad de mapear modelos en la dirección opuesta, podemos incluir el método.ReverseMap().

Una vez que hayamos completado nuestras asignaciones, debemos navegar hasta el archivo Program.cs y registrar AutoMapper con nuestros MappingProfiles.

…
var config = new MapperConfiguration(cfg =>
  {
    cfg.AddProfile(new MappingProfiles());
  });

var mapper = config.CreateMapper();

builder.Services.AddSingleton(mapper);
…

Configurar la autenticación

Usaremos tokens JWT para la autenticación. Nos proporcionan una forma de transmitir información de forma segura entre partes como objeto JSON. Puede leer más sobre los tokens JWT aquí. Para utilizarlos, primero debemos instalar los paquetes de NuGet necesarios. Requerimos tanto Microsoft.IdentityModel.Tokens como Microsoft.AspNetCore.Authentication.JwtBearer.

A continuación, debemos definir algunas configuraciones de token en el archivo appsettings.json. Estas configuraciones incluyen Emisor, Audiencia y SecretKey.

"Jwt": {
  "Issuer": "https://localhost:7244/",
  "Audience": "https://localhost:7244/",
  "Key": "S1u*p7e_r+S2e/c4r6e7t*0K/e7y"
}

Una vez definidas las configuraciones del token, podemos configurar el servicio JWT en el archivo Program.cs. Esto implica especificar el esquema que se utilizará junto con los parámetros de validación necesarios.

…
builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey
        (Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = false,
            ValidateIssuerSigningKey = true
    };
  });
…

Asegúrese de haber agregado también app.UseAuthentication();

Configurando la arrogancia

Para probar los puntos finales de nuestra aplicación usando Swagger UI, debemos incluir app.UseSwaggerUI() en el archivo Program.cs.

Después de eso, debemos generar un filtro AuthResponse para ayudar a probar nuestros puntos finales autenticados utilizando tokens JWT. Para lograr esto, podemos crear una clase AuthResponsesOperationFilter que implemente la interfaz IOperationFilter. El método Apply debe incluir la lógica necesaria para agregar el filtro AuthResponse a Swagger.

public class AuthResponsesOperationFilter: IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
            .Union(context.MethodInfo.GetCustomAttributes(true))
            .OfType<AuthorizeAttribute>();

        if (authAttributes.Any())
        {
            var securityRequirement = new OpenApiSecurityRequirement()
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,
                            Id = "Bearer"
                        }
                    },
                    new List<string>()
                }
            };

            operation.Security = new List<OpenApiSecurityRequirement> {
                securityRequirement
            };

            operation.Responses.Add("401", new OpenApiResponse {
                Description = "Unauthorized"
            });
        }
    }
}

Después de eso, asegúrese de haber agregado el filtro como una opción en el método Program.cs .AddSwaggerGen.

builder.Services.AddSwaggerGen(option =>
    {
        option.SwaggerDoc("v1", new OpenApiInfo {
            Title = "Northwind CRUD", Version = "v1"
        });

        option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            In = ParameterLocation.Header,
            Description = "Please enter a valid token",
            Name = "Authorization",
            Type = SecuritySchemeType.Http,
            BearerFormat = "JWT",
            Scheme = "bearer"
        });

        option.OperationFilter < AuthResponsesOperationFilter > ();
    }
  );

Puede leer una explicación más detallada de "¿Qué es Swagger?" en la documentación oficial.

Registrar punto final

Una vez que hayamos terminado con las configuraciones, podemos continuar con la creación del punto final de registro. El primer paso es generar un archivo RegisterInputModel.cs, que debe ubicarse en la carpeta Models/InputModels.

El proceso de registro requiere los campos Correo electrónico, Nombre, Apellido, Contraseña y Contraseña confirmada. Todos estos campos son obligatorios, por lo que incluiremos el atributo [Obligatorio]. También incluiremos el atributo [Dirección de correo electrónico] para el campo Correo electrónico. Podemos agregar atributos adicionales, como longitud mínima y máxima, según lo deseemos. Sin embargo, a los efectos de esta demostración, nos ceñiremos a estos atributos.

public class RegisterInputModel
{
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    [Required]
    public string Password { get; set; }

    [Required]
    public string ConfirmedPassword { get; set; }
}

A continuación, debemos agregar una asignación al archivo MappingProfiles.cs que permitirá la conversión entre los modelos RegisterInputModel y User en ambas direcciones.

CreateMap<RegisterInputModel, Usuario>().ReverseMap();

Para mantener la separación de inquietudes, crearemos una carpeta de Servicios. Cada módulo tendrá su propio servicio para interactuar con la base de datos. Podemos comenzar generando un archivo AuthService.cs e inyectar DataContext y Configuration.

Nuestro primer método en AuthService.cs debería ser GenerateJwtToken, que toma el correo electrónico y el rol como parámetros y devuelve un token JWT que contiene información del usuario.

public string GenerateJwtToken(string email, string role)
{
    var issuer = this.configuration["Jwt:Issuer"];
    var audience = this.configuration["Jwt:Audience"];
    var key = Encoding.ASCII.GetBytes(this.configuration["Jwt:Key"]);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new []
                {
                    new Claim("Id", Guid.NewGuid().ToString()),
                        new Claim(JwtRegisteredClaimNames.Sub, email),
                        new Claim(JwtRegisteredClaimNames.Email, email),
                        new Claim(ClaimTypes.Role, role),
                        new Claim(JwtRegisteredClaimNames.Jti,
                            Guid.NewGuid().ToString())
                }),
            Expires = DateTime.UtcNow.AddMinutes(5),
            Issuer = issuer,
            Audience = audience,
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);

    return tokenHandler.WriteToken(token);
}

Para codificar la contraseña, usaremos BCrypt.Net.BCrypt. Para comenzar, debemos instalar el paquete y agregarlo como una declaración de uso al comienzo del archivo.

usando BC = BCrypt.Net.BCrypt;

Luego, crearemos varios métodos auxiliares. Uno verificará si existe un usuario con un correo electrónico determinado, otro autenticará al usuario y dos más obtendrán un usuario por correo electrónico e ID.

public bool IsAuthenticated(string email, string password)
{
    var user = this.GetByEmail(email);
    return this.DoesUserExists(email) && BC.Verify(password, user.Password);
}

public bool DoesUserExists(string email)
{
    var user = this.dataContext.Users.FirstOrDefault(x => x.Email == email);
    return user != null;
}

public User GetById(string id)
{
    return this.dataContext.Users.FirstOrDefault(c => c.UserId == id);
}

public User GetByEmail(string email)
{
    return this.dataContext.Users.FirstOrDefault(c => c.Email == email);
}

Antes de crear el método de registro, primero debemos crear un método para generar una identificación única. Este método se puede definir de la siguiente manera:

public class IdGenerator
{
    public static string CreateLetterId(int length)
    {
        var random = new Random();
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

        return new string(Enumerable.Repeat(chars, length)
            .Select(s => s[random.Next(s.Length)]).ToArray());
    }
}

Ahora podemos continuar con la implementación del método Registro. Dentro de este método generaremos un ID único, comprobaremos si ya existe y en caso afirmativo generaremos uno nuevo. Luego, codificaremos la contraseña del usuario y agregaremos el nuevo usuario a la base de datos.

public User RegisterUser(User model)
{
    var id = IdGenerator.CreateLetterId(10);
    var existWithId = this.GetById(id);

    while (existWithId != null)
    {
        id = IdGenerator.CreateLetterId(10);
        existWithId = this.GetById(id);
    }

    model.UserId = id;
    model.Password = BC.HashPassword(model.Password);
    var userEntity = this.dataContext.Users.Add(model);
    this.dataContext.SaveChanges();

    return userEntity.Entity;
}

Si aún no has creado AuthController, ahora es el momento. Vaya a la carpeta Controladores y agregue AuthController, que debería heredar la clase Controlador. También debemos agregar el atributo [ApiController] y [Route(“[controller]”)] para que Swagger lo reconozca.

Después de eso, debemos inyectar el asignador, authService y logger si usamos uno y crear RegisterMethod. Solo los usuarios no autenticados deben poder acceder a la solicitud posterior. Debería aceptar RegisterInputModel como argumento y comprobar si ModelState es válido. Si es así, generará un token jwt para ese usuario. Todo el método debería verse así:

[AllowAnonymous]
[HttpPost("Register")]
public ActionResult<string> Register(RegisterInputModel userModel)
{
    try
    {
        if (ModelState.IsValid)
        {
            if (userModel.Password != userModel.ConfirmedPassword)
            {
                return BadRequest("Passwords does not match!");
            }

            if (this.authService.DoesUserExists(userModel.Email))
            {
                return BadRequest("User already exists!");
            }

            var mappedModel = this.mapper.Map<RegisterInputModel, User>(userModel);
            mappedModel.Role = "User";
            
            var user = this.authService.RegisterUser(mappedModel);
            if (user != null)
            {
                var token = this.authService.GenerateJwtToken(user.Email, mappedModel.Role);
                return Ok(token);
            }
            return BadRequest("Email or password are not correct!");
        }

        return BadRequest(ModelState);
    } catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

Funcionalidad de inicio de sesión

La funcionalidad de inicio de sesión es similar excepto que necesitamos buscar un usuario en la base de datos. Primero debemos crear LoginInputModel.cs que esta vez solo tendrá campos de Correo electrónico y Contraseña. No olvide agregarlo también en MappingProfiles.cs, de lo contrario no funcionará.

public class LoginInputModel
{
    [EmailAddress]
    [Required]
    public string Email { get; set; }

    [Required]
    public string Password { get; set; }
}

Luego, en AuthController.cs, cree el método de inicio de sesión que tomará LoginInputModel como parámetro y verificará si el usuario está autenticado. Si es así, debería generar un token. De lo contrario, debería devolver un error.

[AllowAnonymous]
[HttpPost("Login")]
public ActionResult <string> Login(LoginInputModel userModel)
{
    try
    {
        if (ModelState.IsValid)
        {
            if (this.authService.IsAuthenticated(userModel.Email, userModel.Password))
            {
                var user = this.authService.GetByEmail(userModel.Email);
                var token = this.authService.GenerateJwtToken(userModel.Email, user.Role);

                return Ok(token);
            }

            return BadRequest("Email or password are not correct!");
        }

        return BadRequest(ModelState);

    } 
    catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

Agregar CRUD para eventos

Una vez que hayamos terminado con la autenticación, podemos desarrollar los puntos finales para los eventos. Vamos a crear operaciones CRUD completas. Al igual que con los usuarios, debemos crear el archivo EventService.cs. Incluirá un método para obtener un evento por ID, obtener todos los eventos para un usuario en particular, crear un evento nuevo, actualizar un evento existente y eliminar un evento. Todo el archivo debería verse así:

public class EventService
{
    private readonly DataContext dataContext;

    public EventService(DataContext dataContext)
    {
        this.dataContext = dataContext;
    }

    public Event[] GetAllForUser(string email)
    {
        var user = this.dataContext.Users
            .FirstOrDefault(user => user.Email == email);

        return this.dataContext.Events
            .Include(ev => ev.UserEvents)
            .Where(e => e.UserEvents.FirstOrDefault(ue => ue.UserId == user.UserId) != null)
            .ToArray();
    }

    public Event GetById(string id)
    {
        return this.dataContext.Events
            .Include(ev => ev.UserEvents)
            .FirstOrDefault(c => c.Id == id);
    }

    public Event Create(Event model)
    {
        var id = IdGenerator.CreateLetterId(6);
        var existWithId = this.GetById(id);

        while (existWithId != null)
        {
            id = IdGenerator.CreateLetterId(6);
            existWithId = this.GetById(id);
        }

        model.Id = id;
        var eventEntity = this.dataContext.Events.Add(model);
        this.dataContext.SaveChanges();

        return eventEntity.Entity;
    }

    public Event Update(Event model)
    {
        var eventEntity = this.dataContext.Events
            .Include(ev => ev.UserEvents)
            .FirstOrDefault(c => c.Id == model.Id);

        if (eventEntity != null)
        {
            eventEntity.Title = model.Title != null ? model.Title : eventEntity.Title;

            eventEntity.Date = model.Date != null ? model.Date : eventEntity.Date;

            eventEntity.Category = model.Category != null ? model.Category : eventEntity.Category;

            eventEntity.UserEvents = model.UserEvents.Count! > 0 ? model.UserEvents : eventEntity.UserEvents;

            this.dataContext.SaveChanges();
        }

        return eventEntity;
    }

    public Event Delete(string id)
    {
        var eventEntity = this.GetById(id);

        if (eventEntity != null)
        {
            this.dataContext.Events.Remove(eventEntity);
            this.dataContext.SaveChanges();
        }

        return eventEntity;
    }
}

A continuación, querrás dirigirte al controlador y configurar un método para cada solicitud.

Crearemos un EventBindingModel que se utilizará para almacenar todos los datos necesarios del modelo de evento.

Para el método GetAll, asegúrese de que utiliza una solicitud GET y recupera el token del usuario, lo decodifica y recupera los eventos de ese usuario.

[HttpGet]
[Authorize]
public ActionResult<EventBindingModel[]> GetAll()
{
    try
    {
        var userEmail = this.authService.DecodeEmailFromToken(this.Request.Headers["Authorization"]);
        var events = this.eventService.GetAllForUser(userEmail);
        return Ok(this.mapper.Map<Event[], EventBindingModel[]> (events));

    } catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

…
public string DecodeEmailFromToken(string token)
{
    var decodedToken = new JwtSecurityTokenHandler();
    var indexOfTokenValue = 7;
    var t = decodedToken.ReadJwtToken(token.Substring(indexOfTokenValue));

    return t.Payload.FirstOrDefault(x => x.Key == "email").Value.ToString();
}
…

Obtener por ID también debe ser una solicitud GET con ID como parámetro.

[HttpGet("{id}")]
[Authorize]
public ActionResult<Event> GetById(string id)
{
    try
    {
        var eventEntity = this.eventService.GetById(id);

        if (eventEntity != null)
        {
            return Ok(eventEntity);
        }

        return NotFound();
    } 
    catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

Eliminar el punto final será una solicitud ELIMINAR y también tomará la identificación como argumento.

[HttpDelete("{id}")]
[Authorize(Roles = "Administrator")]
public ActionResult<Event> Delete(string id)
{
    try
    {
        var eventEntity = this.eventService.Delete(id);

        if (eventEntity != null)
        {
            return Ok(eventEntity);
        }

        return NotFound();
    } 
    catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

Para simplificar el proceso de agregar o actualizar registros de eventos, creemos un EventInputModel específicamente para las operaciones de creación y actualización. Este modelo solo requerirá que proporcionemos las propiedades esenciales para userEvents, incluido el título, la categoría, la fecha, el ID de usuario y el ID de evento. Al utilizar este modelo, eliminamos la necesidad de especificar todas las propiedades del modelo de eventos para cada operación.

[HttpPost]
public ActionResult<Event> Create(EventInputModel model)
{
    try
    {
        if (ModelState.IsValid)
        {
            var mappedModel = this.mapper.Map < EventInputModel,
                Event > (model);
            var eventEntity = this.eventService.Create(mappedModel);

            return Ok(eventEntity);
        }

        return BadRequest(ModelState);
    } 
    catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

La actualización será una solicitud PUT y también tomará EventInputModel como parámetro.

[HttpPut]
public ActionResult<Event> Update(EventInputModel model)
{
    try
    {
        if (ModelState.IsValid)
        {
            var mappedModel = this.mapper.Map<EventInputModel, Event>(model);
            var eventEntity = this.eventService.Update(mappedModel);

            if (eventEntity != null)
            {
                return Ok(eventEntity);
            }

            return NotFound();
        }

        return BadRequest(ModelState);
    } 
    catch (Exception error)
    {
        logger.LogError(error.Message);
        return StatusCode(500);
    }
}

Agregar autorización basada en roles

Para restringir ciertas acciones a roles de usuario específicos, podemos usar la autorización basada en roles. En nuestro escenario, por ejemplo, queremos limitar el acceso a los puntos finales Crear, Actualizar y Eliminar para eventos a usuarios con función de Administrador.

Para configurar esto, necesitaremos agregar app.UseAuthorization(); a nuestro archivo Program.cs. Luego, para cada punto final que requiera acceso restringido, agregaremos el atributo [Autorizar], que especificará los roles permitidos. Por ejemplo, podemos asegurarnos de que solo los administradores puedan acceder al punto final Eliminar.

…
[Authorize(Roles = "Administrator")]
public ActionResult<Event> Delete(string id)
…

Creando DbSeeder

Cuando ejecutamos nuestra aplicación, a menudo queremos tener algunos datos precargados en nuestra base de datos, ya sea con fines de prueba u otros motivos. Aquí es donde entra en juego la siembra. Para comenzar, necesitaremos definir los datos que queremos usar.

Para ello podemos crear una carpeta de Recursos y agregar dos archivos JSON: uno para usuarios y otro para eventos. Estos archivos deben contener los datos que queremos completar en la base de datos. Por ejemplo, nuestros archivos podrían verse así:

[
    {
        "UserId": "USERABCDE",
        "FirstName": "Kate",
        "LastName": "Lorenz",
        "Password": "kate.lorenz",
        "Email": "klorenz@hrcorp.com",
        "Role": "Administrator"
    },
    {
        "UserId": "ASERABCDE",
        "FirstName": "Anthony",
        "LastName": "Murray",
        "Password": "anthony.murray",
        "Email": "amurray@hrcorp.com",
        "Role": "User"
    }
]

A continuación, deberíamos crear una clase DbSeeder que incluya un método Seed. Este método leerá los datos que definimos anteriormente y los completará en la base de datos. Para hacer esto, necesitaremos pasar dbContext como parámetro.

public class DBSeeder
{
    public static void Seed(DataContext dbContext)
    {
        ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext));
        dbContext.Database.EnsureCreated();

        var executionStrategy = dbContext.Database.CreateExecutionStrategy();

        executionStrategy.Execute(
            () => {
                using(var transaction = dbContext.Database.BeginTransaction())
                {
                    try
                    {
                        // Seed Users
                        if (!dbContext.Users.Any())
                        {
                            var usersData = File.ReadAllText("./Resources/users.json");
                            var parsedUsers = JsonConvert.DeserializeObject <User[]>(usersData);
                            foreach(var user in parsedUsers)
                            {
                                user.Password = BC.HashPassword(user.Password);
                            }

                            dbContext.Users.AddRange(parsedUsers);
                            dbContext.SaveChanges();
                        }

                        // Seed Events
                        if (!dbContext.Events.Any())
                        {
                            var eventsData = File.ReadAllText("./Resources/events.json");
                            var parsedEvents = JsonConvert.DeserializeObject <Event[]>(eventsData);

                            dbContext.Events.AddRange(parsedEvents);
                            dbContext.SaveChanges();
                        }

                        transaction.Commit();
                    } 
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                    }
                }
            });
    }
}

Después de eso, en nuestra carpeta Helpers, debemos crear una extensión de inicializador de base de datos, que ejecutará el método Seed.

public static class DBInitializerExtension
{
    public static IApplicationBuilder UseSeedDB(this IApplicationBuilder app)
    {
        ArgumentNullException.ThrowIfNull(app, nameof(app));
        using var scope = app.ApplicationServices.CreateScope();

        var services = scope.ServiceProvider;
        var context = services.GetRequiredService < DataContext > ();

        DBSeeder.Seed(context);

        return app;
    }
}

Finalmente, necesitaremos abrir el archivo Program.cs y agregar el método app.UseSeedDB(). Esto asegura que cuando se inicie nuestra aplicación, verificará si hay algún dato en la base de datos. Si no lo hay, el método Seed que creamos anteriormente lo completará automáticamente con los datos que definimos.

Agregar CORS

Para habilitar el intercambio de recursos entre orígenes (CORS) para nuestros puntos finales, necesitaremos agregar un servicio cors en el archivo Program.cs. En este caso, permitiremos el acceso desde cualquier región, pero puedes especificar un dominio específico si lo prefieres.

builder.Services.AddCors(policyBuilder =>
    policyBuilder.AddDefaultPolicy(policy =>
        policy.WithOrigins("*")
        .AllowAnyHeader()
        .AllowAnyHeader())
);

Y luego agregue app.UseCors(); método.

Esto nos permitirá acceder a nuestra API desde una aplicación front-end.

Puede leer más sobre el intercambio de recursos entre orígenes (CORS) aquí.

Para resumir todo esto…

Crear una API basada en roles con ASP .NET Core es un aspecto crucial cuando se trata de crear aplicaciones web seguras y escalables. Al utilizar la autorización basada en roles, puede controlar el acceso a sus recursos API en función de los roles asignados a sus usuarios. Esto garantiza que solo los usuarios autorizados puedan acceder a datos confidenciales o realizar acciones críticas, lo que hace que su aplicación sea más segura y confiable. En este tutorial, vimos cómo crear una API simple basada en roles de 0 a 1. Puede ver el código completo de la demostración en GitHub.

Estén atentos a la Parte 2 de este tutorial, donde demostraremos cómo conectar esa API con App Builder TM y crear una aplicación front-end para ella.

Solicitar una demostración