What every ASP.NET Core Web API project needs - Part 6 - IServiceCollection Extension
Tags: dotnet, aspnetcore, webapi
Published: 2021-04-07 21:06:27

The 5th part of this series was about [Polly](https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-5-polly-1jdf) and this article is about writing extension method for `IServiceCollection`. Honestly not every Web API project needs a service collection extension and if you are ok with the long and messy `Startup` class, you can finish up reading this article.

So far I have added and configured several packages to the [cool-webapi](https://github.com/mo-esmp/cool-webapi) project and in the future articles I will add more packages and configuration and the `Startup` class will become a large class.

Let's clean up [ConfigureServices](https://github.com/mo-esmp/cool-webapi/blob/084b02c9182efcffe471506bbd3d54115af63602/CoolWebApi/Startup.cs#L36) method:
```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddDataAnnotationsLocalization();
    services.AddLocalization(options => options.ResourcesPath = "Resources");

    var supportedCultures = new List<CultureInfo> { new("en"), new("fa") };
    services.Configure<RequestLocalizationOptions>(options =>
    {
        options.DefaultRequestCulture = new RequestCulture("fa");
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;
    });

    services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });

    services.AddApiVersioning(options =>
    {
        // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
        options.ReportApiVersions = true;
    });

    services.AddVersionedApiExplorer(options =>
    {
        // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
        // note: the specified format code will format the version as "'v'major[.minor][-status]"
        options.GroupNameFormat = "'v'VVV";

        // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
        // can also be used to control the format of the API version in route templates
        options.SubstituteApiVersionInUrl = true;
    });

    services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
    services.AddSwaggerGen(options =>
    {
        // add a custom operation filter which sets default values
        options.OperationFilter<SwaggerDefaultValues>();
        options.OperationFilter<SwaggerLanguageHeader>();

        // JWT Bearer Authorization
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.ApiKey
        });
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
                    {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    },
                    Scheme = "oauth2",
                    Name = "Bearer",
                    In = ParameterLocation.Header,
                },
                new List<string>()
            }
                    });

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath);
    });

    var weatherSettings = new WeatherSettings();
    Configuration.GetSection("WeatherSettings").Bind(weatherSettings);
    services.AddSingleton(weatherSettings);

    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);

    services.AddHttpClient<IWeatherHttpClient, WeatherHttpClient>()
        .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
        .AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(6, TimeSpan.FromSeconds(5)))
        .AddPolicyHandler(request =>
        {
            if (request.Method == HttpMethod.Get)
                return timeoutPolicy;

            return Policy.NoOpAsync<HttpResponseMessage>();
        });
}
```

To get started add new class `ServiceCollectionExtensions` to the `Extensions` folder.

- Let's move localization configuration from `ConfigureServices` to an extension method:
```csharp
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddAndConfigureLocalization(this IServiceCollection services)
    {
        services.AddLocalization(options => options.ResourcesPath = "Resources");

        var supportedCultures = new List<CultureInfo> { new("en"), new("fa") };
        services.Configure<RequestLocalizationOptions>(options =>
        {
            options.DefaultRequestCulture = new RequestCulture("fa");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
        });

        return services;
    }
}
``` 

- API versioning configuration:
```csharp
public static IServiceCollection AddAndConfigureApiVersioning(this IServiceCollection services)
{
    services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });

    services.AddApiVersioning(options =>
    {
        // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
        options.ReportApiVersions = true;
    });

    services.AddVersionedApiExplorer(options =>
    {
        // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
        // note: the specified format code will format the version as "'v'major[.minor][-status]"
        options.GroupNameFormat = "'v'VVV";

        // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
        // can also be used to control the format of the API version in route templates
        options.SubstituteApiVersionInUrl = true;
    });

    return services;
}
```
- Swagger:
```csharp
public static IServiceCollection AddAndConfigureSwagger(this IServiceCollection services)
{
    services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
    services.AddSwaggerGen(options =>
    {
        // add a custom operation filter which sets default values
        options.OperationFilter<SwaggerDefaultValues>();
        options.OperationFilter<SwaggerLanguageHeader>();

        // JWT Bearer Authorization
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.ApiKey
        });
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    },
                    Scheme = "oauth2",
                    Name = "Bearer",
                    In = ParameterLocation.Header,
                },
                new List<string>()
            }
        });

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath);
    });

    return services;
}
```
- Weather HTTP client:
```csharp
public static IServiceCollection AddAndConfigureWeatherHttpClient(this IServiceCollection services, IConfiguration configuration)
{
    var weatherSettings = new WeatherSettings();
    configuration.GetSection("WeatherSettings").Bind(weatherSettings);
    services.AddSingleton(weatherSettings);

    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);

    services.AddHttpClient<IWeatherHttpClient, WeatherHttpClient>()
        .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
        .AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(6, TimeSpan.FromSeconds(5)))
        .AddPolicyHandler(request =>
        {
            if (request.Method == HttpMethod.Get)
                return timeoutPolicy;

            return Policy.NoOpAsync<HttpResponseMessage>();
        });

    return services;
}
```

And here is the `ConfigureServices` method after clean up :
```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddDataAnnotationsLocalization();

    services.AddAndConfigureLocalization();

    services.AddAndConfigureApiVersioning();

    services.AddAndConfigureSwagger();

    services.AddAndConfigureWeatherHttpClient(Configuration);
}
```

You can find the source code for this walkthrough on [Github](https://github.com/mo-esmp/cool-webapi).