Install Gdy aplikacja ASP.NET Core jest duża lub jest rozbita na wiele małych kawałków to wtedy warto zastosować wzorzec IServiceCollection. 

Pozwala to tobie rozbić definicje wstrzykiwania to kolejnych serwisów do osobnych klas bądź projektów w solucji. Pisząc złożoną aplikację ASP.NET Core, w którym momencie twoja klasa startup.cs urośnie.

Dlatego pisanie takich swoich rozszerzonych metod wydaje się nieuniknione. Spójrz na ten kod

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddEduZbieraczApplication();
        services.AddEduZbieraczPersistenceEFServices(Configuration);
        services.AddSecurityServices(Configuration);

        services.AddControllers();
    }

Jak widzisz services odwołuje się do pewnych metod, które na pewno nie istnieją w frameworku .NET.

Te rozszerzenia w solucji znajdują się w różnych miejscach i różnych warstwach aplikacji.

Projekt EduZbieracz

W teorii, gdyby chciał jakiś część aplikacji podmienić to w klasie Startup.cs musiałby tylko skasować jedną linijkę przy deklaracji serwisów do wstrzykiwania.

Oto jak wygląda klasa rozszerzeniowa w warstwie Domain

public static class ApplicationInstallation
{
    public static IServiceCollection AddEduZbieraczApplication(this IServiceCollection services)
    {
        services.AddAutoMapper(Assembly.GetExecutingAssembly());
        services.AddMediatR(Assembly.GetExecutingAssembly());

        return services;
    }
}

Jak widzisz w tej warstwie definiuje, że będę używał MediatR oraz Automappera

Zobaczmy jak wygląda to w warstwie Persitence

public static class PersistenceWithEFRegistration
{
    public static IServiceCollection AddEduZbieraczPersistenceEFServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<EduZbieraczContext>(options =>
            options.UseSqlServer(configuration.
            GetConnectionString("EduZbieraczConnectionString")));

        services.AddScoped(typeof(IAsyncRepository<>), typeof(BaseRepository<>));

        services.AddScoped<ICategoryRepository, CategoryRepository>();
        services.AddScoped<IWebinaryRepository, WebinaryRepository>();
        services.AddScoped<IPostRepository, PostRepository>();

        return services;
    }
}

Tutaj definiuje konteksty ADO.NET Entity Framework. Mówie także co implementuje moje interfejsy do obsługi repozytoriów. W tym instalatorze także mówie gdzie w aplikacji ASP.NET CORE będzie Connection String.

Gdyby stwierdził, że chce zmienić technologię dostępu do baz danych np. Dapper. To bym stworzył inny projekt z nowym instalatorem. W aplikacji ASP.NET Core musiałby zmienić tylko 1 linijkę.

Został ostatni Instalator do logowania użytkownika.

public static class EduSecurityServiceExtensions
{
    public static void AddSecurityServices(this IServiceCollection services,
        Microsoft.Extensions.Configuration.IConfiguration configuration)
    {
        services.Configure<JSONWebTokensSettings>
            (configuration.GetSection("JSONWebTokensSettings"));


        services.AddSingleton<IUserManager<EduUser>, UserManager>();
        services.AddSingleton<ISignInManager<EduUser>, SignInManager>();
        services.AddTransient<IAuthenticationService, AuthenticationService>();

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(o =>
            {
                o.RequireHttpsMetadata = false;
                o.SaveToken = false;
                o.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero,
                    ValidIssuer = configuration["JSONWebTokensSettings:Issuer"],
                    ValidAudience = configuration["JSONWebTokensSettings:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JSONWebTokensSettings:Key"]))
                };

                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 500;
                        c.Response.ContentType = "text/plain";
                        return c.Response.WriteAsync(c.Exception.ToString());
                    },
                    OnChallenge = context =>
                    {
                        context.HandleResponse();
                        context.Response.StatusCode = 401;
                        context.Response.ContentType = "application/json";
                        var result = JsonConvert.SerializeObject("401 Not authorized");
                        return context.Response.WriteAsync(result);
                    },
                    OnForbidden = context =>
                    {
                        context.Response.StatusCode = 403;
                        context.Response.ContentType = "application/json";
                        var result = JsonConvert.SerializeObject("403 Not authorized");
                        return context.Response.WriteAsync(result);
                    },
                };
            });
    }
}

W tym instalatorze definiuje zachowanie JSON Web Tokenów oraz implementację logowania się użytkownika. Gdyby chciał to zmienić to by utworzył nowy projekt z nowym instalatorem, a w ASP.NET CORE zmieniłby 1 linijkę kodu.

Ten wzorzec nie tylko sprawia, że klas startup nie tak obładowana, ale także pozwala ci tworzyć klocki lego, które mogą być dołączane do twojej aplikacji ASP.NET CORE. Musi tylko oczywiście widzieli mechanizmy do interfejsów, ale dzisiaj to standard.

public interface IUserManager<TUser> where TUser : class
{
    Task<List<string>> GetRolesAsync(TUser user);
    Task<List<Claim>> GetClaimsAsync(TUser user);
    Task<TUser> FindByEmailAsync(string email);
    Task<TUser> FindByNameAsync(string userName);
    Task<UserManagerResult> CreateAsync(TUser user, string password);
}

public interface ISignInManager<TUser> where TUser : class
{
    Task<SignInResult> PasswordSignInAsync
        (string userName, string password,
        bool isPersistent, bool lockoutOnFailure);
}

public interface IAuthenticationService
{
    Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest request);
    Task<RegistrationResponse> RegisterAsync(RegistrationRequest request);
}

Co mogłem zrobić lepiej? Wypadałoby nazwać wszystkie klasy, które robią rozszerzenia instalacyjne jakąś wspólnym wzorem. Bo jak widzisz te klasy statyczne nazwałem różnie.

W innym projekcie poza stałą nazwą stwierdziłem, że warto zrobić taką klasę statyczną klasą partial.

Projekt GeekLemonConference

Z poziomu ASP.NET CORE tak łatwo znaleźć wszystkie metody instalacyjne.

public static partial class GeekLemonConferenceInstallers
{
    public static IServiceCollection AddGeekLemonConferenceCQRS
        (this IServiceCollection services, IConfiguration Configuration)
    {
        services.AddAutoMapper(Assembly.GetExecutingAssembly());
        services.AddMediatR(Assembly.GetExecutingAssembly());
        services.AddSingleton<IScoringRulesFactory, ScoringRulesFactory>();

        var responseMessagesOption = new ResponseMessagesOption();

        services.AddSingleton<IResponseMessagesOption, ResponseMessagesOption>
            (
                (services) =>
                {
                    return responseMessagesOption;
                }
            );
        BaseResponse.ResponseMessagesOption = responseMessagesOption;

        return services;
    }
}

public static partial class GeekLemonConferenceInstallers
{
    public static IServiceCollection
        AddGeekLemonPersistenceDapperSQLiteServices
        (this IServiceCollection services,
        IConfiguration configuration)
    {
        var connection = configuration.
GetConnectionString("GeekLemonConferenceConnectionString");
        var zEsConnection = configuration.
            GetConnectionString("ZEsGeekLemonConferenceConnectionString");

        services.AddTransient<IGeekLemonDBContext, GeekLemonDBContext>
            (
                (services) =>
                {
                    var c =
                    new GeekLemonDBContext(connection);
                    return c;
                }
            );

        services.AddTransient<IZEsGeekLemonDBContext, ZEsGeekLemonDBContext>
        (
            (services) =>
            {
                var c =
                new ZEsGeekLemonDBContext(zEsConnection);
                return c;
            }
        );

        //If Scoped or Singleton then Two version of repositories will use one version of this
        services.AddTransient<ICallForSpeechGetByIdDoer, CallForSpeechGetByIdDoer>();
        services.AddTransient<ICallForSpeechGetCollectionDoer, CallForSpeechGetCollectionDoer>();
        services.AddTransient<ICallForSpeechSaveAcceptenceDoer, CallForSpeechSaveAcceptenceDoer>();
        services.AddTransient<ICallForSpeechSaveEvaluatationDoer, CallForSpeechSaveEvaluatationDoer>();
        services.AddTransient<ICallForSpeechSavePreliminaryAcceptenceDoer, CallForSpeechSavePreliminaryAcceptenceDoer>();
        services.AddTransient<ICallForSpeechSaveRejectionDoer, CallForSpeechSaveRejectionDoer>();
        services.AddTransient<ICallForSpeechSubmitDoer, CallForSpeechSubmitDoer>();

        services.AddTransient<ICategoryAddDoer, CategoryAddDoer>();
        services.AddTransient<ICategoryGetAllDoer, CategoryGetAllDoer>();
        services.AddTransient<ICategoryDeleteDoer, CategoryDeleteDoer>();
        services.AddTransient<ICategoryGetByIdDoer, CategoryGetByIdDoer>();
        services.AddTransient<ICategoryUpdateDoer, CategoryUpdateDoer>();

        services.AddTransient<IJudgeAddDoer, JudgeAddDoer>();
        services.AddTransient<IJudgeUpdateDoer, JudgeUpdateDoer>();
        services.AddTransient<IJudgeDeleteDoer, JudgeDeleteDoer>();
        services.AddTransient<IJudgeGetAllDoer, JudgeGetAllDoer>();
        services.AddTransient<IJudgeGetByIdDoer, JudgeGetByIdDoer>();

        services.AddTransient<ICategoryRepository, CategoryRepository>();
        services.AddTransient<IJudgeRepository, JugdeRepository>();
        services.AddTransient<ICallForSpeechRepository, CallForSpeechRepository>();
        services.AddTransient<IZEsCategoryRepository, ZEsCategoryRepository>();
        services.AddTransient<IZEsJudgeRepository, ZEsJugdeRepository>();
        services.AddTransient<IZEsCallForSpeechRepository, ZEsCallForSpeechRepository>();

        SqlMapper.RemoveTypeMap(typeof(DateTimeOffset));
        SqlMapper.AddTypeHandler(DateTimeHandler.Default);
        services.AddAutoMapper(Assembly.GetExecutingAssembly());

        return services;
    }
}

public static partial class GeekLemonConferenceInstallers
{
    public static IServiceCollection
        AddEventStoreSqlLite
        (this IServiceCollection services,
        IConfiguration configuration)
    {
        var connection = configuration.
            GetConnectionString("EventStoreSQLiteConnectionString");

        services.AddScoped<IEventStoreSQLiteContext, EventStoreSQLiteContext>
            (
                (services) =>
                {
                    var c =
                    new EventStoreSQLiteContext(connection);
                    return c;
                }
            );

        services.AddScoped<IEventStore, SqlLiteEventStore>();

        return services;
    }
}

Powodzenia w pisaniu dużych rozbity aplikacji w stylu Clean Architecture.