DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on • Updated on

Mejorando la Organización de Código con Vertical Slice Architecture (Parte 6)

Introducción

En este artículo, no presentaremos conceptos revolucionarios, pero es esencial para continuar explorando los aspectos y trucos de ASP.NET. Inicialmente, evitamos complicar las cosas dividiendo todo en proyectos y capas. Sin embargo, al adoptar la Vertical Slice Architecture, encontramos una forma sencilla y bien estructurada de hacerlo.

He abordado este tema en otras ocasiones, y puedes consultar mi repositorio, un vídeo y este artículo donde exploramos en profundidad la Vertical Slice Architecture.

Si deseas obtener más información sobre este tema, te recomiendo visitar esos recursos.

💡 Nota: Puedes encontrar el código correspondiente a este artículo en este repositorio. Y, como siempre, no dudes en contactarme en Twitter.

Refactorizando la Solución

Este conjunto de publicaciones surgió a partir de una idea principal: implementar CQRS y validaciones con FluentValidation (de ahí el nombre de la solución, MediatRValidationExample 🤣). A medida que avanzamos, este proyecto se expandió y ya tengo planeadas 10 o más partes.

Solución original

Actualmente, la solución se presenta como un único proyecto web que alberga todo el código. Aunque está organizado según preocupaciones técnicas, examinemos detenidamente cada elemento para refrescar nuestra comprensión:

Presentación

La capa de presentación o UI, en términos prácticos, abarca todo lo relacionado con la API web. A continuación, un resumen de lo que comprende esta capa:

  • Controladores: Los controladores son una parte integral de la interfaz de usuario, y mantendremos esta estructura intacta en su ubicación actual.
  • Filtros: Los filtros se aplican a los controladores, por lo tanto, están estrechamente relacionados con la presentación.
  • Servicios (en parte): En el directorio "Servicios", encontramos la implementación de CurrentUserService. Esta clase está vinculada al contexto HTTP, pero se ha diseñado con una interfaz para facilitar la abstracción necesaria.
    • Nota: Siempre es fundamental abstraer cuando es necesario, en lugar de crear abstracciones sin un propósito claro.

Application Core

Dentro de la Vertical Slice Architecture, aquí es donde se encuentra el resto de la aplicación. En comparación con la Clean Architecture, aquí abarcamos tanto el dominio (Domain), el núcleo (Core), la persistencia (Persistence) e infraestructura (Infraestructura).

¿Por qué los agrupamos? Este es otro tema que te animo a explorar en el contenido mencionado anteriormente.

En resumen, para el núcleo de la aplicación en su estado actual, tenemos:

  • Behaviours: Decoradores añadidos utilizando MediatR que incorporan reglas de negocio y otras funcionalidades específas de la lógica de la aplicación.
  • Exceptions: Excepciones personalizadas que, al igual que los comportamientos, aportan lógica y reglas al núcleo.
  • Helpers (conocidas como Utils): Este es un elemento un tanto aleatorio que se introdujo en la publicación sobre Hash IDs, pero dado que solo se utilizan en el core, permanece en el core.
  • Dominio: Todo lo relacionado con el dominio (value objects, entities, enums, entity exceptions, domain services, etc.).
  • Features: Todos los segmentos funcionales de la aplicación.
  • Infrastructure: Adaptadores y servicios para comunicarse con servicios externos.
  • Persistence: La base de datos (EF Core).

Una vez que se complete la refactorización, la solución se verá como se muestra a continuación:

Solución refactorizada

Si estás siguiendo estos tutoriales, te recomiendo que no realices la refactorización de inmediato. En cambio, te sugiero descargar el código y analizarlo. Además, ten en cuenta que la parte 7 de esta serie de publicaciones será un tanto diferente 🤭.

Actualización de la Inyección de Dependencias

Ahora permitimos que cada proyecto registre sus propias dependencias (Web y Core), y he añadido dos clases con extensiones para facilitar este proceso: la clase DependencyInjection.

Aquí tienes una versión revisada de los párrafos:

ApplicationCore -> DependencyInjection

using FluentValidation;
using MediatR;
using MediatrExample.ApplicationCore.Common.Behaviours;
using MediatrExample.ApplicationCore.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Reflection;
using System.Text;

namespace MediatrExample.ApplicationCore;
public static class DependencyInjection
{
    public static IServiceCollection AddApplicationCore(this IServiceCollection services)
    {
        services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
        services.AddMediatR(Assembly.GetExecutingAssembly());
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
        services.AddAutoMapper(Assembly.GetExecutingAssembly());

        return services;
    }

    public static IServiceCollection AddPersistence(this IServiceCollection services, string connectionString)
    {
        services.AddSqlite<MyAppDbContext>(connectionString);

        return services;
    }

    public static IServiceCollection AddSecurity(this IServiceCollection services, IConfiguration config)
    {

        services
            .AddIdentityCore<IdentityUser>()
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<MyAppDbContext>();

        services
            .AddHttpContextAccessor()
            .AddAuthorization()
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = config["Jwt:Issuer"],
                    ValidAudience = config["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]))
                };
            });

        return services;
    }
}
Enter fullscreen mode Exit fullscreen mode

Realmente podrías combinar todos estos métodos en uno solo, pero la segmentación actual es útil para comprender su propósito.

Nota 💡: Estas son pautas de referencia. Puedes elegir lo que mejor se adapte a tus necesidades y preferencias. El tema de la Vertical Slice es muy interesante, te animo a investigar más sobre él y explorar los recursos de Jimmy Boggard.

WebApi -> DependencyInjection

using FluentValidation.AspNetCore;
using MediatrExample.ApplicationCore.Common.Interfaces;
using MediatrExample.WebApi.Filters;
using MediatrExample.WebApi.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;

namespace MediatrExample.WebApi;

public static class DependencyInjection
{
    public static IServiceCollection AddWebApi(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "My API",
                Version = "v1"
            });
            c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                In = ParameterLocation.Header,
                Description = "Please insert JWT with Bearer into field",
                Name = "Authorization",
                Type = SecuritySchemeType.ApiKey
            });

            c.AddSecurityRequirement(new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,
                            Id = "Bearer"
                        }
                    },
                    new string[] { }
                }
            });
        });

        services.AddControllers(options =>
            options.Filters.Add<ApiExceptionFilterAttribute>())
            .AddFluentValidation();
        services.Configure<ApiBehaviorOptions>(options =>
            options.SuppressModelStateInvalidFilter = true);

        services.AddScoped<ICurrentUserService, CurrentUserService>();

        return services;
    }
}
Enter fullscreen mode Exit fullscreen mode

Es esencial mantener una organización limpia y legible en tu código, y dividirlo en extensiones facilita la comprensión cuando se revisa el Program.

WebApi -> Program

using MediatrExample.ApplicationCore;
using MediatrExample.ApplicationCore.Domain;
using MediatrExample.ApplicationCore.Infrastructure.Persistence;
using MediatrExample.WebApi;
using Microsoft.AspNetCore.Identity;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddWebApi();
builder.Services.AddApplicationCore();
builder.Services.AddPersistence(builder.Configuration.GetConnectionString("Default"));
builder.Services.AddSecurity(builder.Configuration);

var app = builder.Build();

// Configurar la canalización de solicitudes HTTP.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

await SeedProducts();

app.Run();

// Seed omitido...
Enter fullscreen mode Exit fullscreen mode

La organización del Program ahora es mucho más limpia, y cualquier aspecto específico se puede encontrar fácilmente en la extensión correspondiente.

Conclusión

En resumen, no hay mucho que concluir. Mi objetivo era explicar el proceso de refactorización para las próximas publicaciones, y al principio, mi enfoque era mantenerlo simple.

Siempre he seguido una estructura de carpetas coherente, por lo que esta refactorización no debería presentar ningún problema.

Top comments (9)

Collapse
 
edd profile image
NSysX

Hola, Issac
Tengo una pregunta, en una platica con un companero me dice que es mucho trabajo hacerlo de esta forma de un CRUD por entidad etc.. y me da el siguiente ejemplo. Vamos a suponer que tengo 100 entidades que hay que hacerles el CRUD , Imagina 100 * 4(crud) = 400, me dice no es nada productivo y al no ser productivo no es competitivo. El plantea una forma de programar tipo ERP(asi lo menciono) donde genera un metodo en la entidad base el cual solo se hereda y se usa el metodo y ya hace el insert. Existe algo asi en EF ?

Gracias !!!

Collapse
 
isaacojeda profile image
Isaac Ojeda

Hola!,

La verdad como carezco de contexto, no te sabría decir si es buena idea o no.

Tener muchas entidades obviamente requiere de mucho trabajo, pero no todo es un CRUD, muchas entidades son relaciones con datos adicionales de entidades base (aggregate roots).

Yo tengo un proyecto con más de 150 entidades y no significa que tengo 150 listados con su detalle y creación y edición.

No entiendo muy bien a que te refieres con heredar un método y hacer el INSERT. Sería algo asi como tener un Endpoint donde le digas que Entidad quieres crear y ya le pasas un diccionario de propiedades?

Se puede con reflection, pero, no sé, yo no lo haría así.

Mi pregunta aquí es, ya tienen 100+ entidades y todas van a requerir un CRUD? ahí parten de una suposición y existe el YAGNI (tal vez quieras empezar con algo complejo que tal vez ni siquiera necesites)

Si sí tendrán 100 entidades y todas tienen un CRUD, obviamente sí hay que trabajar algo que se pueda reutilizar y agilizar para no repetir y repetir lo mismo.

Espero mis comentarios random ayuden jaja

Saludos!.

Collapse
 
edd profile image
NSysX

Buen dia, Disculpa la poca info lo que pasa es que fue una platica y para que no se me olvidara lo comentente en un tiempo libre , al lo que mas o menos vi ejejjeje, el tiene un entidad base la cual tiene los metodos de un crud luego creas la entiada por ejemplo Empleado con sus propiedades y la cual hereda de entidad base entonces esta entidad base ya tiene los metodos de un crud (para un repo) disponibles (maneja la reglas de negocio en metodos dentro de la entidad Empleados). Yo habia pensado en Unit of work donde tienes la posibilidad de pasarle la entidad y ya tiene los metodos del crud listos y acepta genericos. Saludos y gracias por contestar.

Collapse
 
jecacarvajal profile image
JecaCarvajal

Buenisimo

Collapse
 
jtempra profile image
Josep Temprà

Muy bueno Isaac...
Sabes de algun ejemplo de app con clean architecture que incorpore API, Fluent Validation, CQRS, Repositorio Generico y el patron specification?
O voy mal encaminado con esta solución?
Muchas grácias!!!

Collapse
 
isaacojeda profile image
Isaac Ojeda

Gracias!!

Hablando de Clean Architecture me gusta siempre basarme en la de Jason Taylor pero no usa specification y el repositorio genérico al usarse Entity Framework, ya viene incluido.

También otra solución que me gusta es la de Ardalis que sí usa specification.

El specification pattern no me gusta usarlo por que aun no he necesitado o descubierto su beneficio en los proyectos que trabajo.

Ardalis tiene muchas platicas, también Jason Taylor, por si gustas verlos

Saludos!!

Collapse
 
jtempra profile image
Josep Temprà

Hola Isaac, muchas gracias por la respuesta...
No soy muy experto y entiendo que cuando dices que EF ya lleva repo generico te refieres a que simplemente trabajas con _context.Entity.****, no?
En cuanto a no usar el especification, como harias una query de una entidad por cualquier combinación de sus propiedades?
Es que a lo mejor no veo la solución facil! jejej
Gracias de nuevo por tu dedicación.
Josep

Thread Thread
 
isaacojeda profile image
Isaac Ojeda

Con gusto!

Y, realmente no entiendo tu pregunta, cuando hago Queries simplemente con Linq los hago justo como lo has visto en los artículos.

Para mi el specification pattern sirve para no estar repitiendo las expressiones en los filtros, en dado caso que estas se usen en distintos lados. Si cambias el specification por requerimiento, afectará todos los lugares donde se usa (que en este caso, es la intención).

Podría ser que hay expressiones muy complicadas y que no necesariamente se re-utilizan, tal vez ahí se podría usar también, pero para mi es mucha "ceremonia" y agrega complejidad (ironía).

Thread Thread
 
isaacojeda profile image
Isaac Ojeda

Y sí, _context.Entity.* yo lo considero el Generic Repository, solo que aquí muchos consideran que estar fuertemente acoplado a Entity Framework es malo, yo en lo personal y en nuestros proyectos, asumimos que nunca cambiaremos de Framework y aunque sí cambiaramos de base de datos, EF soporta muchos motores.