Creando un API RESTFul Completo con Net Core para un Sistema CRM desde cero (4 de 5)
Cuarta entrega: Aprende a hacer una Relación muchos a muchos entre dos entidades y más! 😉

Hey devs! Seguimos trabajando, en esta ocasión con la cuarta de cinco entradas donde estaremos creando un API con arquitectura RESTFul completo para hacer el backend de un sistema de gestión de relación con los clientes o más conocido por sus siglas en inglés: CRM (Customer relationship management). 

Para que no te pierdas, aquí están todas las entradas de la serie:

En esta ocasión aprenderás:

  • Configurar el Fluent API para crear llaves (PK) compuestas (también llamado API Fluente)
  • Crear una relación muchos a muchos entre dos entidades
  • Reforzarás cómo actualizar el contexto, ejecutar migraciones, configurar perfiles de Automapper, usar patrón DTO

Crear entidad AgentesProspectos

Continuando con nuestro modelo de base de datos tenemos una relación muchos a muchos entre Agentes y Prospectos ya que un agente puede tener asignado muchos prospectos, y un prospecto puede tener asignado muchos agentes.

Esto implica que habrá una nueva entidad y la llamaré AgenteProspecto

Entonces en la carpeta Entidades crear la clase AgenteProspecto

    public class AgenteProspecto
    {
        public int AgenteId { get; set; }
        public int ProspectoId { get; set; }
        public int Orden { get; set; }
        public DateTime FechaAsignacion { get; set; }

        //Propiedades de navegación
        public Agente Agente { get; set; }
        public Prospecto Prospecto { get; set; }
    }

Ahora lo que hay que hacer es actualizar las entidades Agente y Prospecto, añadiéndoles las propiedades de navegación correspondientes con la nueva entidad.

Agente:

    public class Agente
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [StringLength(70,ErrorMessage = "El campo {0} no debe exceder de {1} caracteres.")]
        public string Nombre { get; set; }

        //Propiedades de navegación
        public List<AgenteProspecto> AgentesProspectos { get; set; }
    }

Prospecto:

    public class Prospecto
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [StringLength(70, ErrorMessage = "El campo {0} no debe exceder de {1} caracteres.")]
        public string Nombre { get; set; }
        public string UrlPerfil { get; set; }        

        //Propiedades de navegación
        public List<Contacto> Contactos { get; set; }
        public List<AgenteProspecto> AgentesProspectos { get; set; }
    }

Actualizando el contexto

Como ya sabemos cada vez que añadimos o modificamos una entidad, tendremos que actualizar el contexto, en este caso es la clase ApplicationDBContext

    public class ApplicationDBContext : DbContext
    {
        public ApplicationDBContext(DbContextOptions options) : base(options)
        {

        }
        public DbSet<Agente> Agentes { get; set; }
        public DbSet<Prospecto> Prospectos { get; set; }
        public DbSet<Contacto> Contactos { get; set; }
        public DbSet<AgenteProspecto> AgentesProspectos { get; set; }
    }

Configurar el Fuent API

El Fluent API o API fluente como algunos lo llaman, es desde donde se puede configurar y mapear propiedades y tipos, más información aquí.

En este caso configuraremos el Fluent API para hacer que nuestra entidad AgenteProspecto tenga una llave compuesta, una PK compuesta por las propiedades: AgenteId y ProspectoId.

Esta configuración la hacemos desde la clase ApplicationDBContext:

    public class ApplicationDBContext : DbContext
    {
        public ApplicationDBContext(DbContextOptions options) : base(options)
        {

        }

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

            //Configurando PK Compuesta AgenteProspecto
            modelBuilder.Entity<AgenteProspecto>().HasKey(x => new { x.AgenteId, x.ProspectoId });
        }

        public DbSet<Agente> Agentes { get; set; }
        public DbSet<Prospecto> Prospectos { get; set; }
        public DbSet<Contacto> Contactos { get; set; }
        public DbSet<AgenteProspecto> AgentesProspectos { get; set; }
    }

Ahora ejecuta una migración para que puedas enviar esos cambios hacia la base de datos:

add-migration agentesprospectos

update-database

Genial 😎 como podemos ver ya tenemos nuestra tabla y sus relaciones

Actualizando DTOs

Es necesario actualizar algunas de las clases DTO.

Pero antes de empezar a actualizar quiero explicarte el por qué:

Piensa en esto, qué es más lógico?

  • Cuando el usuario cree un nuevo agente se le asignarán sus prospectos
  • Cuando el usuario cree un nuevo prospecto se le asignarán sus agentes

Te hago pensar en esto ya que según respondas a esta cuestión va a variar las propiedades que tengan tus DTO, que a fin de cuentas es el esquema que el usuario a través del API ingresará al sistema.

La respuesta es que es más lógica la segunda opción, cuando uno crea un nuevo prospecto es en ese momento en donde se le asigna su o sus agentes, ya que los agentes es una tabla que no está en constante movimiento, a una empresa con qué frecuencia entra un nuevo vendedor a laborar? uno a la semana, al mes? exactamente no sabemos, pero lo que sí sabemos es que los clientes, en este caso llamados prospectos, se espera que sea una tabla en constante movimiento y crecimiento, por eso elegí la segunda opción.

Entonces vamos a actualizar el ProspectoCreacionDTO

    public class ProspectoCreacionDTO
    {
        [Required]
        [StringLength(70, ErrorMessage = "El campo {0} no debe exceder de {1} caracteres.")]
        public string Nombre { get; set; }
        public string UrlPerfil { get; set; }
        public List<int> AgentesIds { get; set; }
    }

Actualizar perfil de AutoMapper

Tenemos que mapear ahora de ProspectoCreacionDTO hacia Prospecto

En otras palabras desde List<Int> hacia List<AgenteProspecto>

Actualizar la clase AutoMapperProfile

    public class AutoMapperProfile : Profile
    {
        public AutoMapperProfile()
        {
            //Aquí van las reglas de mapeo <origen,destino>
            CreateMap<AgenteCreacionDTO, Agente>();
            CreateMap<Agente, AgenteDTO>();
            //CreateMap<ProspectoCreacionDTO, Prospecto>();
            CreateMap<ProspectoCreacionDTO, Prospecto>()
                .ForMember(x => x.AgentesProspectos, options => options.MapFrom(MapIntToAgenteProspecto));
            CreateMap<Prospecto,ProspectoDTO>();
            CreateMap<ContactoCreacionDTO, Contacto>();
            CreateMap<Contacto, ContactoDTO>();            
        }

        private List<AgenteProspecto> MapIntToAgenteProspecto(ProspectoCreacionDTO prospectoCreacionDTO, Prospecto prospecto)
        {
            List<AgenteProspecto> response = new List<AgenteProspecto>();

            if (prospectoCreacionDTO.AgentesIds == null)
                return response;

            foreach(int id in prospectoCreacionDTO.AgentesIds)
            {                
                response.Add(new AgenteProspecto { AgenteId = id });
            }
            return response;
        }
    }

Actualizando Controlador

Ahora actualizaré el controlador ProspectosController, particularmente el endpoint Post

También incluiremos un método y lo hemos apartado del endpoint ya que se va a utilizar más adelante tambien en el PUT

        [HttpPost]
        public async Task<ActionResult> Post([FromBody] ProspectoCreacionDTO prospectoCreacionDTO)
        {
            if (prospectoCreacionDTO.AgentesIds == null)
                return BadRequest("No se puede insertar un prospecto sin asignarle al menos un agente");

            //obtengo la intersección entre ids recibidos e ids de la base de datos
            var agentesIds = await context.Agentes.Where(x => prospectoCreacionDTO.AgentesIds.Contains(x.Id)).Select(x => x.Id).ToListAsync();

            //Con esto me aseguro que los ids que nos envíen realmente existan
            if (agentesIds.Count != prospectoCreacionDTO.AgentesIds.Count)
                return BadRequest("Se ingresó al menos un agente que no existe");

            var prospecto = mapper.Map<Prospecto>(prospectoCreacionDTO);

            AsignarOrdenAgentes(prospecto);

            context.Prospectos.Add(prospecto);
            await context.SaveChangesAsync();
            return Ok();
        }

        private void AsignarOrdenAgentes(Prospecto prospecto)
        {
            for (int i = 0; i < prospecto.AgentesProspectos.Count; i++)
                prospecto.AgentesProspectos[i].Orden = i;
        }

Hasta el momento podemos hacer unas pruebas en Swagger, corre el proyecto con

CTRL + F5

Probando ingresar ids no existentes de Agentes al insertar un prospecto:

Genial 😉 salta la validación al no existir un agente con id 7

Probando insertar un prospecto:

Genial, esta vez si inserta 😎

De momento es todo ahora te toca que practiques y que hagas más pruebas estimado dev.

Nos vemos en la siguiente entrada para culminar esta serie 😊

A todo dev debe gustarle aprender 😉

Si esta entrada te ha encantado, seguro que sí, entonces ya sabes qué hacer crack! Comparte 😉

2 comentarios en «Creando un API RESTFul Completo con Net Core para un Sistema CRM desde cero (4 de 5)»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *