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.

Discussion (0)