ASP.NET Core - Mantendo o Startup leve e limpo


Que tal manter a classe Startup do seu projeto leve e limpa .

A partir do .NET 6.0 os projetos criados usando os templates do VS 2022 não possuem mais a classe Startup. A inicialização e configuração é feita na classe Program.

Isso não quer dizer que a classe Startup não possa ser usada em seus projetos. Na verdade ela continua sendo suportada no NET 6.0 por uma questão de compatibilidade com os projetos das versões anteriores.

A classe Startup da uma aplicação ASP .NET Core é usada para registrar serviços, repositórios, dependências e fazer a inicialização da aplicação.

Dependendo do projeto pode ocorrer de haver muitas dependências e serviços a serem registradas e assim a classe Startup tende a crescer com muito código definido nos métodos ConfigureServices() e Configure().

E porque você deveria se importar com isso ?

Vou listar alguns motivos:

Com isso em mente vejamos a seguir como manter a classe Startup mais limpa e leve.

Vou apresentar a seguir a classe Startup de um projeto em camadas contendo muito código que será o nosso ponto de partida.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Middleware;
using AutoMapper;
using Core.Repositories;
using Core.Services;
using Core.UnitOfWork;
using Data;
using Data.Repositories;
using Data.UnitOfWork;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Service.Services;
using Microsoft.OpenApi.Models;
using minhaapi.Swagger;

namespace API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

       public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(typeof(Startup));
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
            services.AddScoped(typeof(IInventoryRepository<>), typeof(InventoryRepository<>));
            services.AddScoped(typeof(IWarehouseRepository<>), typeof(WarehouseRepository<>));
            services.AddScoped(typeof(IProductRepository<>), typeof(ProductRepository<>));
            services.AddScoped(typeof(ICustomerRepository<>), typeof(CutomerRepository<>));
            services.AddScoped(typeof(IService<>), typeof(Service<>));
            services.AddScoped(typeof(ICategoryService), typeof(CategoryService));
            services.AddScoped(typeof(IProductService), typeof(ProductService));
            services.AddScoped(typeof(IPersonService), typeof(PersonService));
            services.AddScoped<IUnitOfWork, UnitOfWork>();

            services.AddDbContext<AppDbContext>(options => {
                options.UseSqlServer(Configuration["ConnectionStrings:SqlConStr"].ToString());
            });
            services.AddControllers();
            services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);

            services.AddAuthentication("Bearer")
                    .AddIdentityServerAuthentication(options =>
                    {
                        options.Authority = "https://localhost:44306";
                        options.RequireHttpsMetadata = false;
                        options.ApiName = "my-api";
                    });

            services.AddAuthorization();

            services.AddSwaggerGen(options =>
            {
                options.OperationFilter<SwaggerAuthenticationRequirementsOperationFilter>();

                options.AddSecurityDefinition("My Security Definition", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.OAuth2,
                    BearerFormat = "JWT",
                    In = ParameterLocation.Header,
                    OpenIdConnectUrl = new Uri($"https://localhost:44306/.well-known/openid-configuration"),
                    Flows = new OpenApiOAuthFlows
                    {
                        ClientCredentials = new OpenApiOAuthFlow
                        {
                            AuthorizationUrl = new Uri($"https://localhost:44306/connnect/authorize"),
                            TokenUrl = new Uri($"https://localhost:44306/connect/token"),
                            Scopes = new Dictionary<string, string>
                                {
                                    { "write", "the right to write" },
                                    { "read", "the right to read" }
                                }
                        }
                    }
                });

                var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-fancy-api.xml");

                options.IncludeXmlComments(filePath);
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();           

            app.UseAuthorization();

            app.UseMiddleware<PerformanceMiddleware>();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            app.UseSwagger();

            app.UseSwaggerUI(options => {
                options.SwaggerEndpoint($"/swagger/v1/swagger.json", "V1");
            });
        }
    }
}

Este arquivo até que não esta muito grande, mas já dá para ter uma idéia de que, a medida que mais código for acrescentado ao arquivo mais difícil de manter e entender ele vai ficar.

Podemos então tentar diminuir a quantidade de código neste arquivo.

Mas como fazer isso ?

Usando métodos de extensão

Podemos usar métodos de extensão para tornar o arquivo Startup mais enxuto.

Usando métodos de extensão podemos 'adicionar' métodos a tipos sem criar um novo tipo derivado, nem ter que recompilar ou modificar o tipo original;.

Podemos criar métodos de extensão separados para ter funcionalidades específicas,  e assim, para o nosso caso, podemos criar métodos de extensão para :

  1. Registrar repositórios e dependências
  2. Registrar serviços
  3. Configurar o Swagger
  4. Configurar a Autenticação

E ao final, no arquivo Startup, fazemos a chamada a cada um dos métodos de extensão criados.

Para isso podemos criar em cada projeto uma classe Startup e a seguir criar métodos de extensão para a interface IServiceCollection.

Vamos ao trabalho...

1- Método de extensão ConfigureAppRepositories para registrar os repositórios e dependências

using Microsoft.EntityCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using App.Repositories.Database;
using App.Models.Configuration;

namespace App.Repositories
{
    public static class Startup
    {         
        public static void ConfigureAppRepositories(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddScoped<IUnitOfWork, UnitOfWork>();
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
          services.AddScoped(typeof(IInventoryRepository<>), typeof(InventoryRepository<>));
          services.AddScoped(typeof(IWarehouseRepository<>), typeof(WarehouseRepository<>));
          services.AddScoped(typeof(IProductRepository<>), typeof(ProductRepository<>));
            services.AddScoped(typeof(ICustomerRepository<>), typeof(CutomerRepository<>));

            services.AddDbContext<AppDbContext>(options => {
                options.UseSqlServer(configuration["ConnectionStrings:SqlConStr"].ToString());
            });
        }
    }
}
  

2- Método de extensão ConfigureAppServices para registrar os serviços

using System;
using AutoMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace App.Services {

    public static class Startup
    {
        public static void ConfigureAppServices(this IServiceCollection services, IConfiguration configuration)
        {
            MapperConfiguration config = new MapperConfiguration(cfg => {
                cfg.AddProfile(new AutoMapperProfileConfiguration());
            });
            IMapper mapper = config.CreateMapper();

            services.AddSingleton(mapper);
            services.AddScoped(typeof(IService<>), typeof(Service<>));
            services.AddScoped(typeof(ICategoryService), typeof(CategoryService));
            services.AddScoped(typeof(IProductService), typeof(ProductService));
            services.AddScoped(typeof(IPersonService), typeof(PersonService));
        }
    }
}

3- Método de extensão ConfigureAppSwagger para configurar o Swagger

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using minhaapi.Swagger;

namespace API.Infrastructure
{
    public static class SwaggerExtensions
    {
        public static void ConfigureAppSwagger(this ServiceCollection services, IConfiguration configuration)
        {
            services.AddSwaggerGen(options => {
                options.OperationFilter<SwaggerAuthenticationRequirementsOperationFilter>();

                options.AddSecurityDefinition("My Security Definition", new OpenApiSecurityScheme {
                    Type = SecuritySchemeType.OAuth2,
                    BearerFormat = "JWT",
                    In = ParameterLocation.Header,
                    OpenIdConnectUrl = new Uri($"https://localhost:44306/.well-known/openid-configuration"),
                    Flows = new OpenApiOAuthFlows {
                        ClientCredentials = new OpenApiOAuthFlow {
                            AuthorizationUrl = new Uri($"https://localhost:44306/connnect/authorize"),
                            TokenUrl = new Uri($"https://localhost:44306/connect/token"),
                            Scopes = new Dictionary<string, string>
                                {
                                    { "write", "the right to write" },
                                    { "read", "the right to read" }
                                }
                        }
                    }
                });
                var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-fancy-api.xml");
                options.IncludeXmlComments(filePath);
            });
        }
    }

4 - Método de extensão ConfigureAppAuthentication para configurar a autenticação

public static class AppAuthentication 
{
        public static void ConfigureAppAuthentication(this IServiceCollection services)
        {
                 services.AddAuthentication("Bearer")
                      .AddIdentityServerAuthentication(options =>
                     {
                        options.Authority = "https://localhost:44306";
                        options.RequireHttpsMetadata = false;
                        options.ApiName = "my-api";
                    });

               services.AddAuthorization();
        }
}

Com isso temos 4 métodos de extensão que podemos usar no arquivo Startup do projeto. Fazendo isso iremos obter o resultado abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Middleware;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using App.Services;
using App.Repositories;
using App.Infrastructure.

namespace API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.ConfigureAppRepositories(Configuration);
            services.
ConfigureAppServices(Configuration);

            services.AddControllers();
            services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);

            services.ConfigureAppAuthentication(Configuration);
            services.
ConfigureAppSwagger(Configuration);

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();          

            app.UseAuthorization();
            app.UseMiddleware<PerformanceMiddleware>();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            app.UseSwagger();

            app.UseSwaggerUI(options => {
                options.SwaggerEndpoint($"/swagger/v1/swagger.json", "V1");
            });
        }
    }
}

Temos agora um arquivo Startup mais enxuto com um menor número de instruções using para definir os namespaces e estamos separando as responsabilidades em arquivos distintos.

Uma solução simples mas eficiente.

E estamos conversados...

"Louvai ao SENHOR, porque ele é bom, porque a sua benignidade dura para sempre."
Salmos 118:1

Referências:


José Carlos Macoratti