Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 4 de octubre de 2016
ASP.NET CoreEn ASP.NET Core, sabemos que la clase Startup es donde introducimos el código de inicialización de nuestras aplicaciones. En sus métodos Configure() y ConfigureServices() encontraremos aspectos de todo tipo, como los siguientes, por citar sólo algunos:
  • La configuración del sistema de settings
  • Definición de destinos de logs y niveles de trazas
  • Configuración de componentes de tratamiento de errores y sistemas de depuración y profiling adicionales
  • Configuración de servicios como caching o estado de sesión
  • Inserción y configuración de servicios y middlewares del framework, como los destinados al proceso de contenidos estáticos, internacionalización, CORS, u otros
  • Middlewares personalizados
  • Registro de servicios en el sistema de inyección de dependencias
  • Inicializaciones específicas de la aplicación, como seeds de bases de datos o configuración de mapeos entre objetos
  • Configuración de rutas de la aplicación
Y todo esto, además, salpicado por la lógica para distinguir entre los distintos tipos de entorno de ejecución (development, staging…), o incluso condicionales basados en el contenido de archivos de configuración.

Aunque en aplicaciones pequeñas esto no supondrá ningún problema, cuando nos enfrentamos a sistemas de cierto volumen pronto comenzaremos a ver que la clase de inicialización crece bastante, dificultando su legibilidad y, sobre todo, introduciendo bastante "ruido" sobre el contenido que inicialmente debería presentar. Un ejemplo de ello lo tenemos en el siguiente código, que de hecho está simplificado para que no se alargue demasiado ;)
public void ConfigureServices(IServiceCollection services)
{
    services.AddDirectoryBrowser();
    services.AddSession(opt => { 
       ...// Configurar servicios de estado de sesión
    });
    services.AddRouting(opt => { 
       ...// Configurar servicios de routing
    });
    services.AddDistributedMemoryCache();
    services.AddLocalization(opt => { 
       ... // Configuración de localización
    });
    services.AddAuthorization(configure => { 
       ... // Configuración de servicios de autorización
    });

    services.AddScoped<ICustomerServices, CustomerServices>();
    services.AddScoped<IInvoiceServices, InvoiceServices>();
    services.AddScoped<IOrderServices, OrderServices>();
    services.AddScoped<IDeliveryServices, DeliveryServices>();
    services.AddScoped<INotificationServices, NotificationServices>();
    services.AddScoped<IPaymentServices, PaymentServices>();
    services.AddScoped<ICatalogServices, CatalogServices>();
    services.AddScoped<IProductServices, ProductServices>();
    ... // Registro de otros muchos servicios de la aplicación en el DI

    services.AddSingleton<IMapper>(Mapper.Instance);
    services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler();
    }
    app.UseStatusCodePagesWithReExecute("/error/{0}");

    app.UseStaticFiles();
    app.UseDirectoryBrowser(new DirectoryBrowserOptions() { ... });
    app.UseRequestLocalization(new RequestLocalizationOptions() { ... });
    app.UseCookieAuthentication(new CookieAuthenticationOptions() { ... });
    app.UseSession();

    app.UseCors(builder => { ... });

    app.UseMiddleware<ProfilingMiddleware>();
    app.UseMiddleware<GeolocationMiddleware>();

    ... // Otros middlewares o servicios adicionales de la aplicación

    app.UseMvc(routes =>
    {
        ... // Otras convenciones de rutas
        routes.MapRoute(name: "areas",
            template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
En lugar de llegar a este extremo, es bastante más razonable seguir un criterio como el que ya usábamos con MVC 5 y anteriores, donde el código de inicialización lo encontrábamos en una carpeta elegida por convención (normalmente /App_Start) y estructurado en distintas clases en función del tipo de componente a inicializar.

Para ello, sin duda una buena opción sería construir extensores sobre IServiceCollection e IApplicationBuilder y agrupar en ellos el código de inicialización específico para cada clase. Dado que la convención de ubicación App_Start ya no existe en ASP.NET Core, podríamos elegir cualquier otra que nos interesara.

Por ejemplo, en el caso del registro de servicios en el inyector de dependencias, podríamos crear un extensor personalizado de IServiceCollection e introducir en él todos los registros específicos de nuestra aplicación:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection 
                   AddMyApplicationServices(this IServiceCollection services)
        {
            services.AddScoped<ICustomerServices, CustomerServices>();
            services.AddScoped<IInvoiceServices, InvoiceServices>();
            services.AddScoped<IOrderServices, OrderServices>();
            services.AddScoped<IDeliveryServices, DeliveryServices>();
            services.AddScoped<INotificationServices, NotificationServices>();
            services.AddScoped<IPaymentServices, PaymentServices>();
            services.AddScoped<ICatalogServices, CatalogServices>();
            services.AddScoped<IProductServices, ProductServices>();
            ... // Registro de otros muchos servicios de la aplicación en el DI

            return services;
        }
    }
}
El uso del espacio de nombres Microsoft.Extensions.DependencyInjection es opcional, aunque suele ser una práctica habitual para que intellisense y otras herramientas de ayuda al desarrollo puedan localizar con facilidad esta clase, sin necesidad de tener que añadir usings al archivo.

En cualquier caso, de esta forma simplificaríamos bastante el código del método ConfigureServices() en la clase de inicialización:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDirectoryBrowser();
    services.AddSession(opt => { 
       // Servicios de estado de sesión
    });
    services.AddRouting(opt => { 
       // Servicios de routing
    });
    services.AddDistributedMemoryCache();
    services.AddLocalization(opt => { 
       // Configuración de localización
    });
    services.AddAuthorization(configure => { 
       // Configuración de servicios de autorización
    });

    services.AddMyApplicationServices(); // Mucho mejor ahora :)

    services.AddSingleton<IMapper>(Mapper.Instance);
    services.AddMvc();
}
Asimismo, podríamos crear métodos extensores para configurar distintos grupos de servicios, sobre todo extrayendo aquellos que necesitan más fontanería de inicialización, con lo que conseguiríamos tener un código bastante más limpio:
public void ConfigureServices(IServiceCollection services)
{
    services.AddStaticFilesServices();
    services.AddCachingAndSessionServices();
    services.AddCustomLocalizationServices();
    services.AddCustomAuthorizationServices();
    services.AddMyApplicationServices();
    services.AddMvcServices();
}
Lo mismo que hemos hecho con ConfigureServices() podríamos hacerlo también con el método  Configure(), simplificando así el código de configuración de la aplicación. En este caso, podríamos crear extensores de IApplicationBuilder para agrupar la introducción de middlewares específicos de la aplicación, como en el siguiente código:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ApplicationBuilderExtensions
    {
        public static IApplicationBuilder ConfigureMyCustomMiddlewares(
                  this IApplicationBuilder app)
        {
            app.UseMiddleware<ProfilingMiddleware>();
            app.UseMiddleware<GeolocationMiddleware>();
            ... // Otros middlewares o servicios adicionales de la aplicación
            return app;
        }
    }
}
Podríamos hacer lo mismo, por ejemplo, para extraer y agrupar bloques de configuración de componentes del framework dirigidos a objetivos similares, o con cierta relación entre ellos:
public static IApplicationBuilder ConfigureErrorHandling(
         this IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/error/exception");
    }
    app.UseStatusCodePagesWithReExecute("/error/{0}");
    return app;
}
Y de esta forma, podríamos también dejar el código del método Configure() como los chorros del oro:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.ConfigureDebugging();
    app.ConfigureErrorHandling(env);
    app.ConfigureStaticFiles();
    app.ConfigureAppAuthentication();
    app.ConfigureLocalization();
    app.ConfigureMyCustomMiddlewares();
    app.ConfigureMvc();
}
Publicado en Variable not found.