DEV Community

loading...
Cover image for What every ASP.NET Core Web API project needs - Part 6 - IServiceCollection Extension

What every ASP.NET Core Web API project needs - Part 6 - IServiceCollection Extension

moesmp profile image Mohsen Esmailpour Updated on ・3 min read

The 5th part of this series was about Polly 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 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 method:

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>();
        });
}
Enter fullscreen mode Exit fullscreen mode

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

  • Let's move localization configuration from ConfigureServices to an extension method:
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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • API versioning configuration:
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;
}
Enter fullscreen mode Exit fullscreen mode
  • Swagger:
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;
}
Enter fullscreen mode Exit fullscreen mode
  • Weather HTTP client:
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;
}
Enter fullscreen mode Exit fullscreen mode

And here is the ConfigureServices method after clean up :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddDataAnnotationsLocalization();

    services.AddAndConfigureLocalization();

    services.AddAndConfigureApiVersioning();

    services.AddAndConfigureSwagger();

    services.AddAndConfigureWeatherHttpClient(Configuration);
}
Enter fullscreen mode Exit fullscreen mode

You can find the source code for this walkthrough on Github.

Discussion (2)

pic
Editor guide
Collapse
mwrpwr profile image
Joseph Maurer

I remember learning about Extension methods and thinking that they were amazing, and in practice I find them to do more harm than good. It's the definition of syntactical sugar and gives you the clean appearance that everyone wants, but the problem with it is that it breaks encapsulation. I understand why you used it in this instance, but I think a basic static helper class would be a cleaner implementation. Good Post 👍🏻

Collapse
moesmp profile image
Mohsen Esmailpour Author • Edited

A big drawback of a static method is testability. Mocking static method or extension method is hard, so most of the time developers avoid spreading the business logic into static method or extension method and also there is no much difference between static helper method and extension method:

var a = customer.CreateDate.ToACustomFormat();
var b = DateTimeHelper.ToACustomFormat(customer.CreateDate);
Enter fullscreen mode Exit fullscreen mode

Here is some useful link about extension method:
Extension Methods Guidelines in C# .NET
Extension Methods General Guidelines