-
-
Notifications
You must be signed in to change notification settings - Fork 695
.NET Core WebAPI
Ivan Paulovich edited this page Jan 15, 2020
·
2 revisions
namespace WebApi.DependencyInjection
{
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using WebApi.Filters;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Examples;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
public static class VersionedSwaggerExtensions
{
public static IServiceCollection AddVersionedSwagger(this IServiceCollection services)
{
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'V'VVV");
services.AddSwaggerGen(options =>
{
var provider = services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var apiVersion in provider.ApiVersionDescriptions)
{
ConfigureVersionedDescription(options, apiVersion);
}
var xmlCommentsPath = Assembly.GetExecutingAssembly()
.Location.Replace("dll", "xml");
options.IncludeXmlComments(xmlCommentsPath);
options.OperationFilter<ExamplesOperationFilter>();
options.DocumentFilter<SwaggerDocumentFilter>();
});
return services;
}
private static void ConfigureVersionedDescription(
SwaggerGenOptions options,
ApiVersionDescription apiVersion)
{
var dictionairy = new Dictionary<string, string>
{ { "1.0", "This API features several endpoints showing different API features for API version V1" },
{ "2.0", "This API features several endpoints showing different API features for API version V2" }
};
var apiVersionName = apiVersion.ApiVersion.ToString();
options.SwaggerDoc(apiVersion.GroupName,
new Info()
{
Title = "Clean Architecture Manga",
Contact = new Contact()
{
Name = "@ivanpaulovich",
Email = "ivan@paulovich.net",
Url = "https://github.com/ivanpaulovich"
},
License = new License()
{
Name = "Apache License"
},
Version = apiVersionName,
Description = dictionairy[apiVersionName]
});
}
public static IApplicationBuilder UseVersionedSwagger(
this IApplicationBuilder app,
IApiVersionDescriptionProvider provider)
{
app.UseSwagger(options =>
{
options.PreSerializeFilters.Add((swaggerDoc, httpRequest) =>
{
if (httpRequest.Path.Value.Contains("/swagger"))
{
swaggerDoc.BasePath = httpRequest.Path.Value.Split("/").FirstOrDefault() ?? "";
}
if (httpRequest.Headers.TryGetValue("X-Forwarded-Prefix", out var xForwardedPrefix))
{
swaggerDoc.BasePath = xForwardedPrefix[0];
}
});
});
app.UseSwaggerUI(options =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});
return app;
}
}
}
public sealed class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureDevelopmentServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddControllersAsServices();
services.AddBusinessExceptionFilter();
services.AddFeatureFlags(Configuration);
services.AddVersionedSwagger();
services.AddUseCases();
services.AddInMemoryPersistence();
services.AddPresentersV1();
services.AddPresentersV2();
}
public void ConfigureProductionServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddControllersAsServices();
services.AddBusinessExceptionFilter();
services.AddFeatureFlags(Configuration);
services.AddVersionedSwagger();
services.AddUseCases();
services.AddSQLServerPersistence(Configuration);
services.AddPresentersV1();
services.AddPresentersV2();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseVersionedSwagger(provider);
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
}
public sealed class CustomControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
private readonly IFeatureManager _featureManager;
public CustomControllerFeatureProvider(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
for (int i = feature.Controllers.Count - 1; i >= 0; i--)
{
var controller = feature.Controllers[i].AsType();
foreach (var customAttribute in controller.CustomAttributes)
{
if (customAttribute.AttributeType.FullName == typeof(FeatureGateAttribute).FullName)
{
var constructorArgument = customAttribute.ConstructorArguments.First();
foreach (var argumentValue in constructorArgument.Value as IEnumerable)
{
var typedArgument = (CustomAttributeTypedArgument) argumentValue;
var typedArgumentValue = (Features) (int) typedArgument.Value;
if (!_featureManager.IsEnabled(typedArgumentValue.ToString()))
feature.Controllers.RemoveAt(i);
}
}
}
}
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional : true, reloadOnChange : true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional : true, reloadOnChange : true);
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
// Requires `using Microsoft.Extensions.Logging;`
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.UseStartup(typeof(Program).Assembly.FullName);
}
public static class FeatureFlagsExtensions
{
public static IServiceCollection AddFeatureFlags(this IServiceCollection services, IConfiguration configuration)
{
services.AddFeatureManagement(configuration);
var featureManager = services.BuildServiceProvider()
.GetRequiredService<IFeatureManager>();
services.AddMvc()
.ConfigureApplicationPartManager(apm =>
apm.FeatureProviders.Add(
new CustomControllerFeatureProvider(featureManager)
));
return services;
}
}
public enum Features
{
Transfer,
GetAccountDetailsV2
}
Data Annotations are powerful tool from .NET, it can be interpreted by ASP.NET Core and other frameworks to generate Validation, User Interface and other things. On Manga project, Data Annotations are used to create a complete Swagger UI and HTTP Request validation. Of course following the Clean Architecture Principles we need to keep frameworks under control.
I decided to use Data Annotations on the User Interface layer. Take a look on the RegisterRequest
class:
/// <summary>
/// Registration Request
/// </summary>
public sealed class RegisterRequest
{
/// <summary>
/// SSN
/// </summary>
[Required]
public string SSN { get; set; }
/// <summary>
/// Name
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// Initial Amount
/// </summary>
[Required]
public decimal InitialAmount { get; set; }
}
The RegisterResponse
also needs [Required]
annotation for Swagger Clients.
/// <summary>
/// The response for Registration
/// </summary>
public sealed class RegisterResponse
{
/// <summary>
/// Customer ID
/// </summary>
[Required]
public Guid CustomerId { get; }
/// <summary>
/// SSN
/// </summary>
[Required]
public string SSN { get; }
/// <summary>
/// Name
/// </summary>
[Required]
public string Name { get; }
/// <summary>
/// Accounts
/// </summary>
[Required]
public List<AccountDetailsModel> Accounts { get; }
public RegisterResponse(
Guid customerId,
string ssn,
string name,
List<AccountDetailsModel> accounts)
{
CustomerId = customerId;
SSN = ssn;
Name = name;
Accounts = accounts;
}
}
References: Designing and Testing Input Validation in .NET Core: The Clean Architecture way
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Swagger and API Versioning
- Microsoft Extensions
- Feature Flags
- Logging
- Data Annotations
- Authentication
- Authorization