DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on

 

[Parte 6] ASP.NET: Refactorizando la solución (Vertical Slice Architecture)

Introducción

Este artículo realmente no veremos nada nuevo, pero necesito hacerlo porque vamos a seguir explorando conceptos y trucos en ASP.NET. Al inicio no quería complicarlo dividiendo todo por proyectos y capas. Pero si lo hacemos con Vertical Slice Architecture, será muy sencillo y bien estructurado.

Ya he escrito sobre este tema, tengo un repositorio, un vídeo y este artículo donde exploramos más sobre Vertical Slice Architecture.

Te recomiendo que visites ese contenido si quieres leer más sobre este tema.

Nota💡: Como siempre, aquí te dejo el código que corresponde a este post.
Siempre puedes buscarme en mi twitter.

Refactorizando la Solución

Esta series de post nació de una sola idea, que era implementar CQRS y validaciones con FluentValidation (por eso la solución se llama MediatRValidationExample 🤣) y pues una cosa llevó a otra y ya tengo planeado 10 o más partes.

Solución original

Así está ahorita la solución, un proyecto Web que contiene todo el código. Está dividido por technical concerns eso sí, pero vamos uno por uno que es cada cosa para que nos sirva de repaso:

Presentación

La presentación o UI, en términos prácticos es todo aquello relacionado con la Web API. Aquí el resumen de lo que tenemos perteneciente a este layer:

  • Controllers: Los controllers forman parte de la UI y esta carpeta la dejaremos intacta donde está.
  • Filters: Los filtros se aplican a los controllers, por lo tanto es algo totalmente acoplado a la presentación
  • Services (parcialmente): En Services contamos con la implementación de CurrentUserService. Esta clase está acoplada al HttpContext, pero cuenta con una interfaz para crear la abstracción necesaria.
    • Nota: Siempre es bueno abstraer cuando es necesario, no simplemente crear abstracciones "por que sí"

Application Core

En Vertical Slice Architecture aquí encontraremos el resto de la aplicación, si lo comparamos con Clean Architecture, aquí tendremos tanto Domain, Core, Persistence e Infraestructura

¿Por qué juntos? ese es otro tema, te recomiendo que visites el contenido mencionado anteriormente.

En fin, para el core actualmente tenemos:

  • Behaviours: Los decoradores agregados utilizando MediatR, estos agregan reglas de negocio u otras funcionalidades propias de las reglas de la aplicación.
  • Exceptions: Excepciones custom que al igual que los behaviours, agregan lógica/reglas al core.
  • Helpers (aka Utils): Este es medio random que se agregó en el post de Hash Ids, pero si solo se usan en core, pues se quedan en core.
  • Domain: Todo lo relacionado al dominio (value objects, entities, enums, entity exceptions, domain services, etc)
  • Features: Todos los slices de la aplicación
  • Infrastructure: Adapters / Services para servicios externos
  • Persistence: La base de datos (EF Core)

Por lo que la solución, una vez refactorizada, quedaría así:
Solución refactorizada

Si estas siguiendo estos tutoriales, te recomiendo que no hagas el refactor, es mejor que descargues el código y lo analices, pero trato de explicar el por qué la parte 7 de esta serie de posts, será un poco más diferente 🤭.

Update de Dependency Injection

Ahora dejamos que cada proyecto registre sus dependencias (Web y Core) y agregué dos clases con extensiones para hacerlo. La clase DependencyInjection.

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 aquí pude ponerlo todo dentro del mismo método AddApplicationCore, pero quise hacer distinciones y segmentarlo según su propósito.

Nota 💡: Todo esto es una referencia, puedes tomar lo bueno y lo que consideres malo, dejarlo. El tema Vertical Slice es muy interesante, busca más sobre el tema y 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 exactamente lo mismo, pero antes todo se encontraba en el Program y siempre se empieza a ver feo. Siempre recomendaré separarlo con extensiones para hacerlo más legible y más fácil de entender cuando miras 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();

// Configure the HTTP request pipeline.
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

Program ahora se ve más limpio, cualquier cosa que quieras ver como está configurado, simplemente te metes al método de extensión correspondiente y ya.

Conclusión

Realmente no hay mucho que concluir, solo quise explicar el refactor para los próximos posts y pues realmente al inicio no queria complicarlo, quería hacerlo simple.

Siempre manejé las carpetas correspondientes, por lo que este refactor no debe de presentar problema alguno.

Top comments (6)

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.