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>();
});
}
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;
}
}
- 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;
}
- 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;
}
- 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;
}
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);
}
You can find the source code for this walkthrough on Github.
Top comments (2)
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 👍🏻
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:
Here is some useful link about extension method:
Extension Methods Guidelines in C# .NET
Extension Methods General Guidelines