Creación de una API Rest con Azure Function y arquitectura orientada al dominio de manera local en C#
Este artículo detalla la creación de una API REST con Azure Function y Arquitectura orientada al dominio en C#.
Jhoan Rojas
Jhoan Rojas
Ingeniero de sistemas y desarrollador FullStack con una profunda pasión por la innovación y la tecnología. Comprometido con la creación de software de alta calidad y siempre priorizando las buenas prácticas de código.

Azure Function es un servicio de proceso esencial de microsoft, que se usa para crear Api basadas en REST sin servidor (serverless), que es controlada por eventos que le ayuda de una manera más eficaz y rápida, simplificando la complejidad de la infraestructura y servicios asociados a la misma, centrándose en la lógica de negocio principalmente.

Algunas de las ventajas de utilizar Azure function es:

  • Seguridad proporcionada por Microsoft Azure.
  • Soporte de distintas variedad de lenguajes de programación C#, F#, JavaScript, Java, Python..
  • Monitorización a través de aplicación Insights, para monitorear las funciones de forma sencilla
  • Integración continua y despliegue de código a través de GitHub, Azure DevOps Services y otras herramientas de desarrollo compatibles, propias de azure y de terceros.
  • Escalabilidad según la demanda, gestionando automáticamente la gestión de recursos requeridos
  • Costes basados en consumos

Eventos o Triggers de ejecución de un Azure Function

Estos son los triggers de los que dispone actualmente un Azure Function para su ejecución:

  • HTTPTrigger
  • TimerTrigger
  • CosmosDBTrigger
  • BlobTrigger
  • QueueTrigger
  • EventHubTrigger

En este artículo nos centraremos en el desarrollo de una Api Rest mediante HttpTrigger que se activa mediante solicitudes HTTP.

Vamos a crear una API donde nos permita gestionar nuestros usuarios: agregar, buscarlo por nombre, por id, si está activo y eliminarlos. Es un servicio muy sencillo pero que nos ayudará a entender el funcionamiento y su versatilidad, además que se implementa una arquitectura orientada al dominio siguiendo los principios Solid.

Paso 1 definición del modelo de datos

En este caso se definen las propiedades de la entidad Usuario

Usuario:

  • Name → String
  • Last name → String
  • Email → String
  • Birth Day → DateTime
  • Rol → String
  • Active → bool

Paso 2: Creación del proyecto y definición de capas

Elegimos la creación de un nuevo proyecto y buscamos la opción de una solución en blanco, esto con el fin de empezar a definir las diferentes capas que va a tener la api.

Elegimos un nombre y definimos donde vamos a guardar el proyecto y le damos a crear

Las capas que vamos a trabajar en el proyecto son las siguientes:

  • Users.Api → La capa contendrá:
    • los eventos o HttpTrigger que se crearán en nuestro proyecto.
  • Users.Application → La capa contendrá:
    • Mantendrá la lógica de nuestra aplicación, es conocido como Servicios.
  • Users.Domain → La capa contendrá:
    • modelo de negocio o las entidades.
    • DTO que son datos de transferencia de información, o lo que vamos a recibir por parte del cliente como los datos que nuestro servicio va a mostrar a los usuarios.
  • Users.Infrastructura→ La capa contendrá:
    •  los datos de prueba como una simulación de lectura y escritura de datos, como lo haría una base de datos
    • Capa de Repository que es nuestra capa de persistencia de datos.
    • Mapeo de nuestras entidades a DTO.

Paso 2.1 : Creación de las diferentes capas

Creamos diferentes carpetas en la solución, esto para realizar una separación de cada una de las capas, de esta manera 

  • Creación del HttpTrigger de azure function y ejecución, que nos servirá como api para ser consumida por el cliente 

Seleccionamos la solución, le damos click derecho, agregar, agregar nuevo proyecto y elegimos Azure Function, este proyecto nos proporcionará una plantilla que nos va a proporcionar un Azure Function de ejemplo.

Elegimos un nombre de la azure function, seleccionamos .Net 6 y elegimos que el tipo de “Function” “Http trigger with OpenApi”, esto nos permitira trabajar con una api documentada con swagger, posteriormente elegimos “Authorization level” como “Anonymous”, en la cual no se requiere autorización y cualquiera puede utilizarla. Finalmente le damos crear proyecto.

La creación del proyecto azure function en nuestro proyecto se vería del a siguiente manera:

Archivo Function1.cs:

En la parte superior del editor se puede ejecutar la Azure function, asegurate de que el proyecto esté en User.Function y que aparezca para la ejecución

Una vez ejecutado el proyecto ingresamos al link del RenderSwaggerUI, que nos va a mostrar todos nuestros endpoint creados en Swagger 

Para la creación de las demás capas, seleccionamos la solución, le damos click derecho, agregar, agregar nuevo proyecto y elegimos Aplicación de consola, elegimos un nombre, ubicación de proyecto y la versión en este caso es .Net 6 y le damos crear.

Quedaría de la siguiente manera:

Ya para terminar el tema de la división de capas tenemos que configurar cada uno de los proyecto a excepción de el azure function, como tipo librería de la siguiente manera: Click derecho, propiedades del proyecto y se selecciona en el “Tipo de resultado” como “Biblioteca de clase”:

Paso 3: Desarrollo del Api 

Creación de nuestro Modelo o Entidad

Empezamos definiendo la entidad usuario en la capa de domain, creamos una carpeta que se llame “model” y una clase con el nombre UserModel de esta forma: 


namespace Users.Domain.Model
{
    public class UserModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string LastName {  get; set; }
        public string Email { get; set; }
        public DateTime BirthDay { get; set; }
        public bool IsActive { get; set; }
     
    }
}

Creación de Data de prueba

A Continuación se crea una lista de datos de usuario que va a simular datos de una base de datos en la capa de persistencia, la carpeta se llamará “data”.

Asegúrate de agregar el proyecto que contiene el UserModel en Users.Infraestructure.Persintencia, esto con el fin de utilizar las clases y/o componentes

namespace Users.Infraestructure.Persistence.Data
{
    public  static class DataUsers
    {

        private static List<UserModel> users = new List<UserModel>
        {
            new UserModel() {
                Id=1,
                Name=”Carlos”,
                LastName=”Correa”,
                Email=”example@gmail.com”,
                BirthDay=new DateTime(1998, 01, 10),
                IsActive= true
            },

            new UserModel() {
                Id=2,
                Name=”Julian”,
                LastName=”Rodriguez Cifuentes”,
                Email=”example@gmail.com”,
                BirthDay=new DateTime(1985, 07, 12),
                IsActive= true
           
        }};

    }

}}

Creación de repository y las interfaces

A continuación revisaremos lo que tiene que ver con nuestra capa de persistencia más concretamente con los repository, que es una capa que gestiona la data externa, por ejemplo el de una base de datos.

Se crearán unas interfaces que tiene algunas firmas o métodos sin implementación, esto con el fin de poder abstraer nuestra implementación y basarnos en un principios solid llamado Inversión de Dependencias, donde las abstracciones no deberían depender de los detalles de la implementación concreta.

 para mas información puede consultar aqui : “https://profile.es/blog/principios-solid-desarrollo-software-calidad/

IUsersRepository:

Task hace referencia a que es un método asíncrono, lo que es un proceso que no va a estar bloqueando la aplicación y que por consiguiente va a enviar los datos cuando los tenga listos. Esto comúnmente se utiliza con recursos externos, como base de datos o peticiones a Apis externas, en este caso estamos simulando una petición a una base de datos.

namespace Users.Infraestructure.Persistence.IRepository
{
    public interface IUsersRepository
    {
        public Task<List<UserModel>> GetUsers();
        public Task<UserModel> GetUserById(int id);
        public Task<UserModel> GetUserByName(string name);

        public Task<List<UserModel>> IsActive();
        public Task<UserModel> IsNotActive();

        public Task<UserModel> CreateUser(UserModel user);

    }
}

Se implementa la interfaz IUsersRepository en la clase UsersRepository, y se implementa todos los métodos, como contrato de implementación.

UsersRepository:

Se hace la implementación de los métodos tales como la creación de usuario, la búsqueda por Id, nombre, usuarios activos, inactivos y todos los usuarios, lo lógica detrás de la implementación son propios de las listas.

namespace Users.Infraestructure.Persistence.Repository
{
    public class UsersRepository : IUsersRepository
    {
        public async Task<UserModel> CreateUser(UserModel user)
        {
            user.Id=DataUsers.users.Count+1;

            DataUsers.users.Add(user);

            return user;
         
        }

        public async Task<UserModel?> GetUserById(int id)
        {
            var user= DataUsers.users.FirstOrDefault(x => x.Id==id);

            return user ?? null;

        }

        public async Task<UserModel?> GetUserByName(string name)
        {
            var user =
            DataUsers.users.FirstOrDefault(
            x => x.Name.ToLower() ==name.ToLower());
            return user ?? null;
        }

        public async Task<List<UserModel>> GetUsers()
        {
            return DataUsers.users;
        }

        public async Task<List<UserModel>> IsActive()
        {
            var userAcive =
            DataUsers.users.Where(x => x.IsActive==true).ToList();
            return userAcive;
        }

     

      public async Task<List<UserModel>> IsNotActive()
        {
            var userInactive =
            DataUsers.users.Where(x => x.IsActive == false).ToList();
            return userInactive;
        }
    }
}

Creación del serivicios y su interfaz

La capa de servicio es la capa que concentra toda la lógica de la aplicación, estamos hablando de validaciones y transformaciones necesarias que requiere tu aplicación para funcionar.

Empezamos creando una interfaz que nos va a servir como un contrato para el servicio, casi identico a la capa de repository, recibe los mismo parámetros, con la única diferencia que es información que vamos a mostrar a los clientes, así que tenemos que tener mucho cuidado con los datos que vamos a exponer, no queremos que todos los datos de nuestra aplicación tales como estadísticas o métricas las conozca el usuario o también en el caso de que se requiera una transformación de los datos, por lo que se hace la implementación de un DTO (Data Transfer Object).

Para la creación nos vamos a la capa Domain y creamos una carpeta que se llamara “Dto” y una clase dentro de la carpeta que se va a llamar UserDto, de esta manera.

Como se puede observar en la clase Dto a diferencia del userModel, en mi aplicación no quiero mostrar al cliente la fecha de nacimiento, es un dato que puede llegar a ser sensible, por lo que lo omitiremos aunque a la hora de crear dicho usuario si la necesitaremos.

namespace Users.Domain.Dto
{
    public class UserDto
    {
     
            public int Id { get; set; }
            public string Name { get; set; }
            public string LastName { get; set; }
            public string Email { get; set; }
            public bool IsActive { get; set; }
    }
}

Para la creación de los servicios necesitamos un Dto que se llame como creationUserDto, es la información que vamos a recibir del cliente para poder crear un usuario, de nuevo, no ocupamos el userModel por que no sería buena idea exponer nuestro modelo y además tiene información que no se le pedirá al usuario como lo es el Id, asi que quedaria de esta manera:

namespace Users.Domain.Dto
{
    public class CreateUserDto
    {
        public string Name { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime BirthDay { get; set; }
        public bool IsActive { get; set; }
    }
}

Creación de IUserService:

Como se puede observar lo que cambia es el tipo de datos que devuelve, y recibe, hablando del método createUser.

namespace Users.Application.IServices
{
    public interface IUserService
    {
        public Task<List<UserDto>> GetUsers();
        public Task<UserDto?> GetUserById(int id);
        public Task<UserDto?> GetUserByName(string name);

        public Task<List<UserDto>> IsActive();
        public Task<List<UserDto>> IsNotActive();

        public Task<UserDto> CreateUser(CreateUserDto user);
    }
}

Implementación de UserService

Para la creación de los diferentes servicios necesitaremos de una instancia de la capa de datos, para esto vamos a utilizar algo conocido como inyección de dependencia, específicamente en .Net el framework directamente nos va a suministrar una instancia de la capa de repository cuando la necesitemos, con esto evitamos hacer instancias directas en nuestro servicio y no generamos un acoplamiento alto, además que cualquier cambio en la implementación no debería impactar significativamente en la capa de servicios

Para que .Net framework nos suministre una instancia de la capa repository haremos lo siguiente:

Crearemos una propiedad de solo lectura con el tipo de la interfaz, además que ese mismo tipo de datos lo vamos a pedir en el constructor de nuestra clase, quedando de la siguiente manera:

namespace Users.Application.Services
{
    public class IUserRepository : UserRepository
    {
        private readonly IUserRepository _userRepository;
        public UserService(IUserRepository userRepository) {
            _userRepository = userRepository;
        }
    }}

Existen tres tipos de inyección de dependencias en .Net:

  • Singleton: .Net nos suministra una instancia por todo el ciclo de vida de la aplicación
  • Transient : .Net nos suministra una instancia por cada solicitud
  • Scoped: .Net nos suministra una instancia por cada solicitud en el ámbito de la solicitud actual.

En este caso para la capa de datos vamos a utilizar Singleton, por qué es data local o de la aplicación, pero normalmente cuando estamos conectados a una base de datos es de suma importancia que sea Transient, evitando problemas de concurrencia y de compartir estados de usuarios.

Para que .Net sepa como suministrar la instancia y cuando, se hace lo siguiente:

Se descarga la siguiente dependencia:

Se tiene que tener en cuenta que dicha dependencia sea descargado en el proyecto de User.function

Se crea una clase que se va a llamar FunctionStartup y se sobreescribe el método Configure, con el fin de poder hacer la inyección de dependencias de esta manera:

[assembly: FunctionsStartup(typeof(User.Function.FunctionStartup))]
namespace User.Function
{

    public class FunctionStartup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
         
        }
    }
}

Ya posteriormente se agrega el tipo de inyección de dependencias:

Como se puede observar si se llegara a cambiar la implementación el cambio solo se haría en un solo lugar. 

 para más información consulta:  https://www.compartimoss.com/revistas/numero-48/azure-functions-best-practices/

Hay algo que tenemos que hacer antes de implementar la lógica de nuestro servicio, vamos a necesitar la ayuda un mapper, esto con el fin de poder convertir un tipo UserModel a un tipo UserDto y viceversa ,lo anterior se podría ser de manera manual, pero cada cambio por mas minimo que sea, en alguna de las clases,  se tendría que hacer en muchas parte de nuestra aplicación, en cambio el mapeo se ajusta a los tipo. 

Vamos a crear en la capá de persistencia una carpeta que se va a llamar Mapping y una clase que se va a llamar AutoMapperProfiles

Junto con una dependencia para esa capa que se llama:

Agregamos la configuración de mepro en la clase de AutoMapperProfile:

En este caso en el primer mapeo vamos a convertir de UserModel a UserDto y de UserDto a User model por eso agregamos el ReverseMap(), en el segundo caso, recibimos el Dto y lo transformamos en un UserModel.

namespace Users.Infraestructure.Persistence.Mappings
{
    public class AutoMapperProfile: Profile
    {
      public AutoMapperProfile() {

            CreateMap<UserModel, UserDto>().ReverseMap();
            CreateMap<CreateUserDto, UserModel>();
        }
    }
}

Configuramos el archivo de FunctionStartup para que podamos recibir una instancia de la misma, cuando se mande a llamar:

Ya finalizando nos vamos a la capa de Service y le indicamos que necesitamos una instancia de AutoMapper para poderla trabajar en el Service

namespace Users.Application.Services
{
    public class UserService : IUserService
    {
        private readonly IUsersRepository _userRepository;
        private readonly IMapper _mapper;
        public UserService(IMapper mapper,                          IUsersRepository userRepository) {
            _mapper = mapper;
            _userRepository = userRepository;
        }

     
    }
}

Implementaremos la capa de Service: 

A la hora de hacer el mapeo, el tipo es decir: _mapper.Map<UserDto>(user); UserDto es lo que espero recibir  y user  es lo que yo voy a enviar, es decir recibo un Dto y envío un tipo  de UserModel, de esta manera se hace el mapeo, y es el mismo caso para la lista de datos.

namespace Users.Application.Services
{
    public class UserService : IUserService
    {
        private readonly IUsersRepository _userRepository;
        private readonly IMapper _mapper;
        public UserService(IMapper mapper,                           IUsersRepository  userRepository) {
            _mapper = mapper;
            _userRepository = userRepository;
        }

        public async Task<UserDto> CreateUser(CreateUserDto user)
        {
            var userModel = _mapper.Map<UserModel>(user);
            await _userRepository.CreateUser(userModel);
            return _mapper.Map<UserDto>(userModel);
        }

        public async Task<UserDto?> GetUserById(int id)
        {
            var user= await _userRepository.GetUserById(id);
            var userDto= _mapper.Map<UserDto>(user);
            return userDto;

        }

        public async Task<UserDto?> GetUserByName(string name)
        {
            var user = await _userRepository.GetUserByName(name);
            var userDto = _mapper.Map<UserDto>(user);
            return userDto;
        }

        public async Task<List<UserDto>> GetUsers()
        {
            var users = await _userRepository.GetUsers();
            return _mapper.Map<List<UserDto>>(users);
        }

        public async Task<List<UserDto>> IsActive()
        {
            var userActive= await _userRepository.IsActive();
            var userActiveDto = _mapper.Map<List<UserDto>>(userActive);
            return userActiveDto;
        }

        public  async Task<List<UserDto>> IsNotActive()
        {
            var userNotActive = await _userRepository.IsNotActive();
            var userInactiveDto = _mapper.Map<List<UserDto>>                                  (userNotActive);
            return userInactiveDto;
        }
    }
}

Ya por último nos concentramos en las diferentes funciones que se van a trabajar, para eso necesitamos la capa de servicio, por lo que repetiremos la inyección de dependencias para la capa service:

Function GetAllUsers:

Para esta función tenemos lo que es el FunctionName  que es el nombre de la function, continuamos con el OperationId, este Id es para la documentación de Swagger como también el tag que es de usuario, continuamos tenemos el estado de una respuesta satisfactoria, que seria Ok, el content type, el tipo de contenido que es un json y el tipo de cuerpo que devuelve que en este caso sería una List<UserDto> o cualquier tipo de datos, este saldrá como una documentación de la api. 

Seguimos con la especificación como parámetro de HttpTrigger donde el nivel de autorización es Anonymous y los métodos que se van a utilizar en dicha función, en este caso funcionaria con get y post, y el HttpRequest que es toda la información de la petición

Function GetUserById

En este caso particular necesitamos habilitar la entrada de parámetros, para poder recibir el id del usuario y así poderlo consultar, para ello le decimos a Swagger con el OpenApiParametor el nombre del parámetro que vamos a utilizar, en este caso “id”, ya siguiendo definimos el Route, que es la ruta y por último lo recibimos como parámetro de tipo entero, validamos si el usuario existe, si no enviamos una Excepción.

Function CreateUser:

En esta Function habilitaremos el poder enviar información por el cuerpo de la petición y de tipo Json, por lo que se agrega la anotación [OpenApiRequestBody] para decirle a Swagger que va recibir los datos por el cuerpo, y definimos también el tipo de cuerpo que va a tener, cambiamos el tipo de petición por un “post”, pondremos el path de ruta, y por último le indicaremos que tipo de dato recibimos, en este caso un tipo CreateUser, eliminando req, ya que no lo necesitaremos.

Ya los demás se implementan de la misma manera, simplemente se cambiaría el nombre de la función y se definirá si requiere parámetro y el tipo de dato los parámetros según sea el caso.

Implementación de las otras funciones:

UserActive:

UserInactive:

GetUserByName:

Deja un comentario

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

Blog

Contáctanos