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 馃槈

3 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 *