Creando un API RESTFul Completo con Net Core para un Sistema CRM desde cero (5 de 5)
Quinta y última entrega: Data relacionada con Include y ThenInclude, optimización y buenas prácticas 💪

Y llegamos a la última entrega de esta serie donde creamos 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:

  • Traer data relacionada con Include y ThenInclude
  • Optimizar la data relacionada
  • Aplicar buenas prácticas a tus controladores en todos los verbos HTTP

Traer data relacionada

Actualmente cuando obtengo la data de un prospecto, sólo me trae data de él:

Sin embargo quiero que también me traiga data de los agentes, para esto entonces vamos a hacer trabajar a las propiedades de navegación y la data fluirá desde el prospecto, pasará por AgentesProspectos y finalmente llegará hasta Agentes.

El primer paso para lograr esto es Actualizar los DTO:

Actualizando ProspectoDTO:

    public class ProspectoDTO
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public string UrlPerfil { get; set; }
        public List<AgenteDTO> Agentes { get; set; }
    }

Actualizando AgenteDTO:

    public class AgenteDTO
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public List<ProspectoDTO> Prospectos { get; set; }
    }

Para continuar debemos ahora actualizar los perfiles de AutoMapper, esto lo hacemos como sabemos en la clase AutoMapperProfiles

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

            CreateMap<Prospecto, ProspectoDTO>()
                .ForMember(x => x.Agentes, options => options.MapFrom(MapFromAgentesProspectosToAgenteDTO));

            CreateMap<ContactoCreacionDTO, Contacto>();
            CreateMap<Contacto, ContactoDTO>();            
        }

        private List<ProspectoDTO> MapFromAgentesProspectosToProspectoDTO(Agente agente, AgenteDTO agenteDTO)
        {
            List<ProspectoDTO> response = new List<ProspectoDTO>();
            if (agente.AgentesProspectos == null)
                return response; //no hacemos validaciones adicionales para respetar el principio SRP de responsabilidad única

            foreach (var item in agente.AgentesProspectos)
                response.Add(new ProspectoDTO { Id = item.ProspectoId, Nombre = item.Prospecto.Nombre, UrlPerfil = item.Prospecto.UrlPerfil });

            return response;
        }

        private List<AgenteDTO> MapFromAgentesProspectosToAgenteDTO(Prospecto prospecto, ProspectoDTO prospectoDTO)
        {
            List<AgenteDTO> response = new List<AgenteDTO>();
            if (prospecto.AgentesProspectos == null)
                return response; //no hacemos validaciones adicionales para respetar el principio SRP de responsabilidad única

            foreach (var item in prospecto.AgentesProspectos)
                response.Add(new AgenteDTO { Id = item.AgenteId, Nombre = item.Agente.Nombre });

            return response;
        }

        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;
        }
    }

Tenemos que actualizar ahora los controladores ProspectosController y AgentesController

Particularmente el endpoint Get(id)

        [HttpGet("{id:int}")]
        public async Task<ActionResult<ProspectoDTO>> Get(int id)
        {
            var prospecto = await context.Prospectos
                .Include(x=>x.AgentesProspectos)
                .ThenInclude(x=>x.Agente)
                .FirstOrDefaultAsync(x => x.Id == id);

            if (prospecto == null)
                return NotFound("Registro no encontrado.");

            prospecto.AgentesProspectos = prospecto.AgentesProspectos.OrderBy(x => x.Orden).ToList(); //ordenando la lista por el campo orden
            return mapper.Map<ProspectoDTO>(prospecto);
        }
        [HttpGet("{id:int}")]
        public async Task<ActionResult<AgenteDTO>> Get(int id)
        {
            var agente = await context.Agentes
                .Include(x=>x.AgentesProspectos)
                .ThenInclude(x=>x.Prospecto)
                .FirstOrDefaultAsync(x=>x.Id == id);

            if (agente == null)
                return NotFound("Registro no encontrado.");

            return mapper.Map<AgenteDTO>(agente);
        }

Hacemos la prueba en Swagger y genial 😎 ahora nos trae la información del agente!

Probando el controlador de prospectos
Probando el controlador de agentes

Ahora hay un detalle, si te das cuenta existe una referencia cíclica y por eso es que en agentes tenemos null, que no es gran cosa, podríamos dejarlo así sin embargo te enseñaré a corregirlo, como para dejar todo listo y bien configurado:

Lo que tienes que hacer es aprovechar la Herencia en los DTO, esto quiere decir que vamos a actualizar los DTO ProspectoDTO y AgenteDTO:

    public class ProspectoDTO
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public string UrlPerfil { get; set; }
        //public List<AgenteDTO> Agentes { get; set; }
    }
    public class AgenteDTO
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        //public List<ProspectoDTO> Prospectos { get; set; }
    }

Creamos dos nuevos DTO: ProspectoConAgentesDTO y AgenteConProspectosDTO y cada uno heredará de ProspectoDTO y AgenteDTO respectivamente

    public class ProspectoConAgentesDTO: ProspectoDTO
    {
        public List<AgenteDTO> Agentes { get; set; }
    }
    public class AgenteConProspectosDTO : AgenteDTO
    {
        public List<ProspectoDTO> Prospectos { get; set; }
    }

Actualizar perfil de AutoMapper:

   public AutoMapperProfile()
    {
        //Aquí van las reglas de mapeo <origen,destino>
        CreateMap<AgenteCreacionDTO, Agente>();
        CreateMap<Agente, AgenteDTO>();
        CreateMap<Agente, AgenteConProspectosDTO>()
            .ForMember(x => x.Prospectos, options => options.MapFrom(MapFromAgentesProspectosToProspectoDTO));

        CreateMap<ProspectoCreacionDTO, Prospecto>()
            .ForMember(x => x.AgentesProspectos, options => options.MapFrom(MapIntToAgenteProspecto));

        CreateMap<Prospecto, ProspectoDTO>();
        CreateMap<Prospecto, ProspectoConAgentesDTO>()
            .ForMember(x => x.Agentes, options => options.MapFrom(MapFromAgentesProspectosToAgenteDTO));

        CreateMap<ContactoCreacionDTO, Contacto>();
        CreateMap<Contacto, ContactoDTO>();            
    }

Actualizar los métodos Get(id) de AgentesController y ProspectosController

        [HttpGet("{id:int}")]
        public async Task<ActionResult<AgenteConProspectosDTO>> Get(int id)
        {
            var agente = await context.Agentes
                .Include(x=>x.AgentesProspectos)
                .ThenInclude(x=>x.Prospecto)
                .FirstOrDefaultAsync(x=>x.Id == id);

            if (agente == null)
                return NotFound("Registro no encontrado.");

            return mapper.Map<AgenteConProspectosDTO>(agente);
        }
        [HttpGet("{id:int}")]
        public async Task<ActionResult<ProspectoConAgentesDTO>> Get(int id)
        {
            var prospecto = await context.Prospectos
                .Include(x=>x.AgentesProspectos)
                .ThenInclude(x=>x.Agente)
                .FirstOrDefaultAsync(x => x.Id == id);

            if (prospecto == null)
                return NotFound("Registro no encontrado.");

            prospecto.AgentesProspectos = prospecto.AgentesProspectos.OrderBy(x => x.Orden).ToList(); //ordenando la lista por el campo orden
            return mapper.Map<ProspectoConAgentesDTO>(prospecto);
        }

Ahora probamos los controladores Prospectos y Agentes con los datos de prueba que tengo

Completando las Operaciones HTTP con buenas prácticas

Hasta ahora estamos retornando Ok() en nuestros endpoints POST, sin embargo una buena práctica de la arquitectura REST es retornar más que sólo eso, debemos retornar objetos estándar, solucionemos esto con el uso de CreatedAtRoute:

Lo primero que hay que hacer es asegurarse que todos los endpoints de tipo Get(id) tengan nombre

Al de ProspectosController le falta, entonces le ponemos nombre:

       [HttpGet("{id:int}", Name = "ObtenerProspecto")]
        public async Task<ActionResult<ProspectoConAgentesDTO>> Get(int id)
        {
            //...
        }

Lo mismo hacemos con AgentesController:

        [HttpGet("{id:int}", Name = "ObtenerAgentes")]
        public async Task<ActionResult<AgenteConProspectosDTO>> Get(int id)
        {
            //...
        }

Añadimos GetById(id) al controlador ContactosController ya que no existe:

        [HttpGet("{id:int}", Name = "ObtenerContacto")]
        public async Task<ActionResult<ContactoDTO>> GetById(int prospectoId, int id)
        {
            var contacto = await context.Contactos.Where(x => x.ProspectoId == prospectoId).FirstOrDefaultAsync(x => x.Id == id);

            if (contacto == null)
                return NotFound("Contacto no existe");

            return mapper.Map<ContactoDTO>(contacto);
        }

Actualizando endpoints POST

En ProspectosController:

        [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();

            var prospectoDTO = mapper.Map<ProspectoDTO>(prospecto);
            return CreatedAtRoute("ObtenerProspecto", new { id = prospecto.Id }, prospectoDTO);
        }

En AgentesController:

        [HttpPost]
        public async Task<ActionResult> Post([FromBody] AgenteCreacionDTO agenteCreacionDTO)
        {
            var existe = await context.Agentes.AnyAsync(x => x.Nombre == agenteCreacionDTO.Nombre);
            if (existe)            
                return BadRequest($"Ya existe un agente con el nombre {agenteCreacionDTO.Nombre}");

            var agente = mapper.Map<Agente>(agenteCreacionDTO);
            context.Add(agente);
            await context.SaveChangesAsync();

            var agenteDTO = mapper.Map<AgenteDTO>(agente);
            return CreatedAtRoute("ObtenerAgentes", new { id = agente.Id }, agenteDTO);
        }

En ContactosController:

        [HttpPost]
        public async Task<ActionResult> Post(int prospectoId, ContactoCreacionDTO contactoCreacionDTO)
        {
            var existeProspecto = await context.Prospectos.AnyAsync(x => x.Id == prospectoId);

            if (!existeProspecto)
                return NotFound("El prospecto no existe");

            var contacto = mapper.Map<Contacto>(contactoCreacionDTO);
            contacto.ProspectoId = prospectoId;

            context.Contactos.Add(contacto);
            await context.SaveChangesAsync();

            var contactoDTO = mapper.Map<ContactoDTO>(contacto);
            return CreatedAtRoute("ObtenerContacto", new { id = contacto.Id, prospectoId = prospectoId }, contactoDTO);
        }

Probamos en Swagger:

Genial, mira que ahora nos devuelve más información, como debe ser 😎

Te toca a ti probar los otros controladores crack.

Creando endpoints PUT

Aquí vamos a reutilizar los DTO de creacion ya que los campos son los mismos 😉

El endpoint PUT de Prospectos es un caso especial ya que a diferencia de los otros 2 queremos que no sólo actualice sus campos sino también sus autores

En ProspectosController

        [HttpPut("{id:int}")]
        public async Task<ActionResult> Put(int id, ProspectoCreacionDTO prospectoCreacionDTO)
        {
            var prospecto = await context.Prospectos.Include(x => x.AgentesProspectos).FirstOrDefaultAsync(x => x.Id == id); //así traigo el prospecto y también sus agentes

            if (prospecto == null)
                return NotFound("El prospecto no existe");

            prospecto = mapper.Map(prospectoCreacionDTO, prospecto); //mapeando objetos existentes de origen y destino

            AsignarOrdenAgentes(prospecto);

            context.Prospectos.Update(prospecto);
            await context.SaveChangesAsync();
            return NoContent();
        }

En AgentesController

        [HttpPut("{id:int}")]
        public async Task<ActionResult> Put(int id, AgenteCreacionDTO agenteCreacionDTO)
        {
            var existeAgente = await context.Agentes.AnyAsync(x => x.Id == id);
            if (!existeAgente)
                return NotFound("No existe el agente");

            var agente = mapper.Map<Agente>(agenteCreacionDTO);
            agente.Id = id;

            context.Update(agente);
            await context.SaveChangesAsync();
            return NoContent();
        }

En ContactosController

        [HttpPut("{id:int}")]
        public async Task<ActionResult> Put(int id, int prospectoId, ContactoCreacionDTO contactoCreacionDTO)
        {
            var existeProspecto = await context.Prospectos.AnyAsync(x => x.Id == prospectoId);
            if (!existeProspecto)
                return NotFound($"El prospecto con id {prospectoId} no existe");

            var existeContacto = await context.Contactos.AnyAsync(x => x.Id == id);
            if (!existeContacto)
                return NotFound($"El contacto con id {id} no existe");

            var contacto = mapper.Map<Contacto>(contactoCreacionDTO);
            contacto.Id = id;
            contacto.ProspectoId = prospectoId;

            context.Contactos.Update(contacto);
            await context.SaveChangesAsync();
            return NoContent();
        }

Creando endpoints DELETE

En ProspectosController

        [HttpDelete("{id:int}")]
        public async Task<ActionResult> Delete(int id)
        {
            var existeProspecto = await context.Prospectos.AnyAsync(x => x.Id == id);
            if (!existeProspecto)
                return NotFound("El prospecto no existe");

            context.Prospectos.Remove(new Prospecto { Id = id });
            await context.SaveChangesAsync();
            return NoContent();
        }

En AgentesController

        [HttpDelete("{id:int}")]
        public async Task<ActionResult> Delete(int id)
        {
            var existeAgente = await context.Agentes.AnyAsync(x => x.Id == id);
            if (!existeAgente)
                return NotFound("El agente no existe");

            context.Agentes.Remove(new Agente { Id = id });
            await context.SaveChangesAsync();
            return NoContent();
        }

En ContactoController

        [HttpDelete("{id:int}")]
        public async Task<ActionResult> Delete(int id, int prospectoId)
        {
            var existeContacto = await context.Contactos.Where(x => x.ProspectoId == prospectoId).AnyAsync(x => x.Id == id);//debo validar que el contacto pertenezca al prospecto especificado
            if (!existeContacto)
                return NotFound("El contacto no existe o no coincide con el prospecto especificado");

            context.Contactos.Remove(new Contacto { Id = id });
            await context.SaveChangesAsync();
            return NoContent();
        }

Si te das cuenta no hemos hecho el controlador StatusController, eso te lo dejo de tarea, ya que con todo lo que hemos vista hasta aquí no deberías tener problema en hacerlo, ya conoces cómo hacer una relación uno-a-muchos 😎

Eso es todo crack! ahora toca sus ricas pruebas con Swagger 😉

Haré algunas pruebas más básicas sin embargo puedes hacer todas las pruebas exhaustivas que creas conveniente ya que el API RESTful ya está completo 😉

Intentaré, insertar, obtener, actualizar y eliminar un agente:

Ahora lo actualizaré:

Y ahora lo reporto, genial!

Vamos a eliminarlo ahora:

Ahora pruebo el Get(id) y genial, funciona todo! 😎

Las validaciones están OK 😊

Todo el proyecto avanzado hasta aquí lo tengo en mi cuenta de Github, disponible en: https://github.com/GeaSmart/CRM-API-v1

Hay algunas ideas que te puedo dar para que por tu cuenta trates de hacer este API más completo, inténtalo, así se aprende:

  • Crear el controlador StatusController con sus respectivos endpoints
  • Implementar el endpoint PATCH
  • Configurar ambientes con IConfiguration y variables de ambiente
  • Implementar Identity para manejo de seguridad y tokens de sesiones
  • Manejar versiones de tu API en Swagger
  • Hacer paginaciones
  • Implementar Unit Testing
  • Desplegar tu API en IIS y Azure
  • Configurar tu pipeline CI/CD (Devops)

Como verás hay varias cosas que te harán un developer experto, sin embargo no te abrumes, que no son cosa del otro mundo, y en posts sucesivos trataré estos temas también así que comparte y mantente atento siempre mi estimado crack! 💪🔥

Terminó la serie y ha sido todo un placer, nos vemos en la siguiente entrada!

La pelota ⚽ está ahora en tu cancha crack!

Si esta serie tan provechosa te ha servido bastante, entonces ya sabes qué hacer dev! Compártela 😉

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

Deja una respuesta

Tu dirección de correo electrónico no será publicada.