Introduction
Two months ago, ApiBoilerPlate was first released and it’s incredible to see that the template garnered hundreds of installs within a short period of time. I’m very glad that it somehow benefited some developers, so thank you for the support. I’m very excited to announce that the new version of ApiBoilerPlate
has been released recently. In this post, we’ll take a look at the new features added to the template.
What ApiBoilerPlate Is?
ApiBoilerPlate
is a simple yet organized project template for building ASP.NET Core APIs using .NET Core 3.x (the latest/fastest version of .NET Core to date) with preconfigured tools and frameworks. It features most of the functionalities that an API will have such as database CRUD operations, Token-based Authorization, Http Response format consistency, Global exception handling, Logging, Http Request rate limiting, HealthChecks and many more. The goal is to help you get up to speed when setting up the core structure of your app and its dependencies when spinning up a new ASP.NET Core API project. This enables you to focus on implementing business specific code requirements without you having to copy and paste the core structure of your project, common features, and installing its dependencies all over again. This will speed up your development time while enforcing standard project structure with its dependencies and configurations for all your apps.
Key Takeaways
Here's the list of the good stuff that you can get when using the template:
- Configured Sample Code for database CRUD operations
- Configured Basic Data Access using Dapper
- Configured Logging using
Serilog
- Configured
AutoMapper
for mapping entity models to DTOs - Configured
FluentValidation
for DTO
validations - Configured
AutoWrapper
for handling request exceptions and consistent Http
response format - Configured
AutoWrapper.Server
for unwrapping the Result attribute from AutoWrapper's ApiResponse
output - Configured
Swagger
API Documentation - Configured
CORS
- Configured
JWT
Authorization and Validation - Configured Sample Code for Requesting Client Credentials Token
- Configured
Swagger
to secure API documentation with Bearer
Authorization - Configured Sample Code for connecting Protected External APIs
- Configured Sample Code for implementing custom API Pagination
- Configured
HttpClient
Resilience and Transient fault-handling - Configured
Http
Request Rate Limiter - Configured
HealthChecks
and HealthChecksUI
- Configured Unit Test Project
- Configured Sample Code for Worker service. For handling extensive process in the background, you may want to look at the Worker Template created by Jude Daryl Clarino. The template was also based on
ApiBoilerPlate
.
How to Get It?
There are two ways to install the template:
For installation steps, visit the following links:
What Was Changed?
I personally like keeping things simple, clean and organize. The new version (v2
) of the template has been reorganized to simplify the folder structure groupings and refactored to provide much cleaner code. The main thing that was changed is moving Configurations
, Extensions
, Filters
, Handlers
, Helpers
and Installers
folders into a new folder called Infrastructure
. A few of the folders was new for v2
and this is to organize files needed for your application without mixing them with one another to value the separation of concerns and ease of maintainability.
Some services that were configured in the Startup.cs file were moved to a dedicated class file under the Infrastructure/Installers folder. This will keep the Startup.cs file leaner and enables you to have a dedicated file for configuring each middleware
.
Another thing that was changed is merging the Domain folder into the Data folder to simplify things a bit. In the Entity folder, you will see a new class called "EntityBase"
to provide a base class that houses common properties for your entity classes.
The DTO (a.k.a Data Transfer Object) folder has been reorganized as well to split Request
and Response
objects. This means that each request dto should have its own class and each response dto should have it own class and specific validation rules as well. This is to decouple them from the entity class (a.k.a Models
) so that when a requirement changes or if your entity properties change, they won't be affected and wont break your API
. Your entity classes should only be used for database related process and your DTOs are for mapping the requests and response objects from your entity classes and only expose properties that you want your client to see.
I’ve also added a couple of methods in PersonManager.cs class to demonstrate paging and executing queries with transaction.
Finally, all Nuget
dependencies have been updated to most recent versions.
What Was Added?
I put together all requests I gathered in version 1 from the community feedback and added a few more features for version 2 as well. Here’s the list of newly added features:
- Enable
CORS
JWT
Authorization and Validation - Sample Code for Requesting Client Credentials Token
Swagger
to secure API documentation with Bearer
Authorization - Sample Code for connecting Protected External APIs
- Sample Code for implementing custom API Pagination
HttpClient
Resilience and Transient fault-handling Http
Request Rate Limiter HealthChecks
and HealthChecksUI
- Unit Test Project
Enable CORS
Cross-Origin Resource Sharing (a.k.a. CORS) enable clients that are hosted in different domains/ports accessing your API endpoints. The template was configured to allow any origin, header and method as shown in the code below:
services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
You may need to change the default policy configuration to allow only specific origins, headers and methods based on your business requirements. For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core.
IdentityServer4 JWT Authentication
The template uses IdentityServer4
to authenticate and validate access tokens. You can find the code that configures IdentityServer
Authentication under Installers/RegisterIdentityServerAuthentication.cs file. Here’s the code snippet:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = config["ApiResourceBaseUrls:AuthServer"];
options.RequireHttpsMetadata = false;
options.ApiName = "api.boilerplate.core";
});
The code above adds Authentication support using "Bearer"
as the default scheme. It then configures IdentityServer
Authentication handler. The Authority
is the base Url to where your IdentityServer
is hosted. The ApiName
should be registered in your IdentityServer
as an Audience
. The RequireHttpsMetadata
property is turned off by default and you should turn it on when you deploy the app in production.
When your APIs are decorated with the [Authorize]
attribute, then the requesting clients should provide the access token generated from IdentityServer
and pass it as a Bearer Authorization Header
before they can be granted access to your API endpoints. For more information, see: IdentityServer: Protecting APIs.
Sample Code for Requesting Client Credentials Token
It occurred to me that accessing protected internal/external services are pretty much a common scenario and so I have decided to include a sample code demonstrating how to do it in ASP.NET Core. In version 2, you can see a new folder called "
Services"
and under it, you can find a class called AuthServerConnect
with the following code:
public class AuthServerConnect : IAuthServerConnect
{
private readonly HttpClient _httpClient;
private readonly IDiscoveryCache _discoveryCache;
private readonly ILogger<AuthServerConnect> _logger;
private readonly IConfiguration _config;
public AuthServerConnect(HttpClient httpClient, IConfiguration config,
IDiscoveryCache discoveryCache, ILogger<AuthServerConnect> logger)
{
_httpClient = httpClient;
_config = config;
_discoveryCache = discoveryCache;
_logger = logger;
}
public async Task<string> RequestClientCredentialsTokenAsync()
{
var endPointDiscovery = await _discoveryCache.GetAsync();
if (endPointDiscovery.IsError)
{
_logger.Log(LogLevel.Error, $"ErrorType: {endPointDiscovery.ErrorType}
Error: {endPointDiscovery.Error}");
throw new HttpRequestException
("Something went wrong while connecting to the AuthServer Token Endpoint.");
}
var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync
(new ClientCredentialsTokenRequest
{
Address = endPointDiscovery.TokenEndpoint,
ClientId = _config["Self:Id"],
ClientSecret = _config["Self:Secret"],
Scope = "SampleApiResource"
});
if (tokenResponse.IsError)
{
_logger.Log(LogLevel.Error, $"ErrorType: {tokenResponse.ErrorType}
Error: {tokenResponse.Error}");
throw new HttpRequestException
("Something went wrong while requesting Token to the AuthServer.");
}
return tokenResponse.AccessToken;
}
}
The code snippet above requests an access token from IndentityServer
Token
endpoint by passing the registered client_id
, client_secret
and scope
.
The RequestClientCredentialsTokenAsync()
method will then be called each time you issue an Http
Requests via HttpClient
. This process is encapsulated in a custom bearer token DelegatingHandler
class called ProtectedApiBearerTokenHandler
. Here’s the code snippet:
public class ProtectedApiBearerTokenHandler : DelegatingHandler
{
private readonly IAuthServerConnect _authServerConnect;
public ProtectedApiBearerTokenHandler(IAuthServerConnect authServerConnect)
{
_authServerConnect = authServerConnect;
}
protected override async Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
var accessToken = await _authServerConnect.RequestClientCredentialsTokenAsync();
request.SetBearerToken(accessToken);
return await base.SendAsync(request, cancellationToken);
}
}
We can then register the ProtectedApiBearerTokenHandler
as a Transient
service in onfigureServices()
method in Startup.cs:
services.AddTransient<ProtectedApiBearerTokenHandler>();
Sample Code for Accessing Protected Internal/External APIs
Under Services folder, you can find the SampleApiConnect.cs class that houses a couple of methods for requesting external services or APIs as shown in the following code:
namespace ApiBoilerPlate.Services
{
public class SampleApiConnect: IApiConnect
{
private readonly HttpClient _httpClient;
private readonly ILogger<SampleApiConnect> _logger;
public SampleApiConnect(HttpClient httpClient,ILogger<SampleApiConnect> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<SampleResponse>
PostDataAsync<SampleResponse, SampleRequest>(string endPoint, SampleRequest dto)
{
var content = new StringContent(JsonSerializer.Serialize(dto),
Encoding.UTF8, HttpContentMediaTypes.JSON);
var httpResponse = await _httpClient.PostAsync(endPoint, content);
if (!httpResponse.IsSuccessStatusCode)
{
_logger.Log(LogLevel.Warning, $"[{httpResponse.StatusCode}]
An error occured while requesting external api.");
return default(SampleResponse);
}
var jsonString = await httpResponse.Content.ReadAsStringAsync();
var data = Unwrapper.Unwrap<SampleResponse>(jsonString);
return data;
}
public async Task<SampleResponse> GetDataAsync<SampleResponse>(string endPoint)
{
var httpResponse = await _httpClient.GetAsync(endPoint);
if (!httpResponse.IsSuccessStatusCode)
{
_logger.Log(LogLevel.Warning, $"[{httpResponse.StatusCode}]
An error occured while requesting external api.");
return default(SampleResponse);
}
var jsonString = await httpResponse.Content.ReadAsStringAsync();
var data = Unwrapper.Unwrap<SampleResponse>(jsonString);
return data;
}
}
}
To take advantage of Dependency Injection, we can then register a typed instance of HttpClientFactory
for SampleApiConnect
class and then pass in the ProtectedApiBearerTokenHandler
as a Message Handler. Here’s the code snippet that you can find under Infrastructure/Installer/ RegisterApiResources.cs file:
services.AddHttpClient<IApiConnect, SampleApiConnect>(client =>
{
client.BaseAddress = new Uri(config["ApiResourceBaseUrls:SampleApi"]);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue(HttpContentMediaTypes.JSON));
})
.AddHttpMessageHandler<ProtectedApiBearerTokenHandler>();
This way, when we issue an Http
Requests to SampleApiConnect
endpoints, it automatically generates an access token for us and sets it as a Bearer Authentication
header every time we invoke an Http
call.
Here’s the code for calling the SampleApiConnect
methods that you can find under API/v1/SampleApiControlle.cs file:
public class SampleApiController : ControllerBase
{
private readonly ILogger<SampleApiController> _logger;
private readonly IApiConnect _sampleApiConnect;
public SampleApiController
(IApiConnect sampleApiConnect, ILogger<SampleApiController> logger)
{
_sampleApiConnect = sampleApiConnect;
_logger = logger;
}
[Route("{id:long}")]
[HttpGet]
public async Task<ApiResponse> Get(long id)
{
if (ModelState.IsValid)
return new ApiResponse
(await _sampleApiConnect.GetDataAsync<SampleResponse>($"/api/v1/sample/{id}"));
else
throw new ApiException(ModelState.AllErrors());
}
[HttpPost]
public async Task<ApiResponse> Post([FromBody] SampleRequest dto)
{
if (ModelState.IsValid)
return new ApiResponse(await _sampleApiConnect.PostDataAsync
<SampleResponse, SampleRequest>("/api/v1/sample", dto));
else
throw new ApiException(ModelState.AllErrors());
}
}
HttpClient Resilience and Transient Fault-Handling
When you call internal or external services within your API
app, there is the ever-present risk when communicating with services over a transport such as Http
that a transient fault will occur. A transient fault may prevent your request from being completed, but is also likely to be a temporary problem.
The template uses Polly to enable us to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. The template uses the following features:
Retry
- maybe it's a network blip Circuit-breaker
- Try a few times, but stop so you don't overload the system Timeout
- Try, but give up after n seconds/minutes
You can find how Polly
was configured under Infrastructure/Installer/RegisterApiResources.cs file. Here’s the code snippet:
var policyConfigs = new HttpClientPolicyConfiguration();
config.Bind("HttpClientPolicies", policyConfigs);
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>
(TimeSpan.FromSeconds(policyConfigs.RetryTimeoutInSeconds));
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => r.StatusCode == HttpStatusCode.NotFound)
.WaitAndRetryAsync(policyConfigs.RetryCount, _ => TimeSpan.FromMilliseconds
(policyConfigs.RetryDelayInMs));
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(policyConfigs.MaxAttemptBeforeBreak,
TimeSpan.FromSeconds(policyConfigs.BreakDurationInSeconds));
var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();
services.AddTransient<ProtectedApiBearerTokenHandler>();
services.AddHttpClient<IApiConnect, SampleApiConnect>(client =>
{
client.BaseAddress = new Uri(config["ApiResourceBaseUrls:SampleApi"]);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue(HttpContentMediaTypes.JSON));
})
.SetHandlerLifetime(TimeSpan.FromMinutes(policyConfigs.HandlerTimeoutInMinutes))
.AddHttpMessageHandler<ProtectedApiBearerTokenHandler>()
.AddPolicyHandler(request => request.Method == HttpMethod.Get? retryPolicy : noOpPolicy)
.AddPolicyHandler(timeoutPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
The code snippet above defines a set of Polly
polices for Retries
, CircuitBreaker
and Timeout. In this example, we’ve applied Retry policy for GET
request only for idempotency reason. It will trigger a Retry every 500 milliseconds for 3 times. We’ve also applied circuitbreaker to allow 3 attempts and blocks execution for 30 seconds on the 4th attempt. Finally, we’ve setup a Timeout
when the execution goes beyond a certain threshold, in this case, a 5 second timeout. This guarantees the caller won't have to wait beyond the timeout.
Here’s the HttpClientPolicies
configuration which can be found within appsettings.TEST.json file:
"HttpClientPolicies": {
"RetryCount": 3,
"RetryDelayInMs": 500,
"RetryTimeoutInSeconds": 5,
"BreakDurationInSeconds": 30,
"MaxAttemptBeforeBreak": 3,
"HandlerTimeoutInMinutes": 5
}
Http Request Rate Limiter
In order to prevent your API endpoints from being abused, we usually enforce a rate limit on the number of requests that a client can consume over a time period. Throttling the API endpoint on the server side can protect our system from overloading resources which deteriorates the performance of the API endpoint.
The template uses AspNetCoreRateLimit
that provides a solution designed to control the rate of requests that clients can make to your APIs based on IP address or client ID. You can find how it was implemented under Infrastructure/Installer/RegisterRequestRateLimiter.cs file. Here’s the code snippet:
internal class RegisterRequestRateLimiter : IServiceRegistration
{
public void RegisterAppServices(IServiceCollection services, IConfiguration config)
{
services.AddOptions();
services.AddMemoryCache();
services.Configure<IpRateLimitOptions>(config.GetSection("IpRateLimiting"));
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
}
}
And here’s the IpRateLimiting
configuration in appsettings.TEST.json file:
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"GeneralRules": [
{
"Endpoint": "*:/api/*",
"Period": "1s",
"Limit": 2
}
]
}
The configuration above defines the rule for every endpoint requests that contains the segment "/api/"
. The client can only request an endpoint 2 times within a 1 second period. Of course, you are free to change the configuration that best suits your needs.
The EnableEndpointRateLimiting
needs to be set to true
so that IP rate limits are applied to specific endpoints like "*:/api/*"
rather than all endpoints ("*"
). In the GeneralRules
section, we set one rate limiting rule. The rule says, for endpoint like "*:/api/*"
, only allow 2 requests within every 1 second. The format of Endpoint
means that for any Http
verb ("*:"
), all URLs that start with "/api/"
and end with anything ("*"
) will comply with the rule.
Here’s a sample screenshot of the captured requests of 200
and 429
Http
StatusCodes.
For the failed API request, the response contains an exception message of "API calls quota exceeded! maximum admitted 2 per 1s.
" and an Http
status code 429
Too Many Requests. The response headers include a key-value pair of "Retry-After: 1
", which instructs consumers to retry after 1 second in order to overcome the rate limit.
For more information, see: https://github.com/stefanprodan/AspNetCoreRateLimit and Changhui Xu's excellent article about Requests Rate Limiting.
HealthChecks and HealthChecksUI
Great systems are built to anticipate and handle unexpected issues, rather than just silently failing.
The template uses HealthChecks
to monitor the health of the app. This enable us to monitor the status of our application dependencies such as database connection, external services and many more. HealthChecks
keep us alerted as soon as something isn't functioning well or some services are unavailable, rather than hearing the issues from a customer.
You can find a sample HealthChecks
configuration under Infrastructure/Installers/RegisterHealthChecks.cs file. Here’s the code snippet:
services.AddHealthChecks()
.AddCheck("Google Ping", new PingHealthCheck("www.google.com", 100))
.AddCheck("Bing Ping", new PingHealthCheck("www.bing.com", 100))
.AddUrlGroup(new Uri(config["ApiResourceBaseUrls:AuthServer"]),
name: "Auth Server",
failureStatus: HealthStatus.Degraded)
.AddUrlGroup(new Uri(config["ApiResourceBaseUrls:SampleApi"]),
name: "External Api",
failureStatus: HealthStatus.Degraded)
.AddNpgSql(config["ConnectionStrings:PostgreSQLConnectionString"],
name: "PostgreSQL",
failureStatus: HealthStatus.Unhealthy)
.AddSqlServer(
connectionString: config["ConnectionStrings:SQLDBConnectionString"],
healthQuery: "SELECT 1;",
name: "SQL",
failureStatus: HealthStatus.Degraded,
tags: new string[] { "db", "sql", "sqlserver" });
services.AddHealthChecksUI();
It uses the following Nuget
packages from AspNetCore.Diagnostics.HealthChecks
to perform basic HealthCheck
monitoring:
AspNetCore.HealthChecks.SqlServer
AspNetCore.HealthChecks.Npgsql
AspNetCore.HealthChecks.Uris
I’ve also included a simple PingHealthCheck
to add as an example. Here’s the code snippet:
internal class PingHealthCheck : IHealthCheck
{
private string _host;
private int _timeout;
public PingHealthCheck(string host, int timeout)
{
_host = host;
_timeout = timeout;
}
public async Task<HealthCheckResult> CheckHealthAsync
(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
using (var ping = new Ping())
{
var reply = await ping.SendPingAsync(_host, _timeout);
if (reply.Status != IPStatus.Success)
{
return HealthCheckResult.Unhealthy
($"Ping check status [{ reply.Status }]. Host
{_host} did not respond within {_timeout} ms.");
}
if (reply.RoundtripTime >= _timeout)
{
return HealthCheckResult.Degraded
($"Ping check for {_host} takes too long to respond.
Expected {_timeout} ms but responded in {reply.RoundtripTime} ms.");
}
return HealthCheckResult.Healthy($"Ping check for {_host} is ok.");
}
}
catch
{
return HealthCheckResult.Unhealthy
($"Error when trying to check ping for {_host}.");
}
}
}
In the Configure()
method of Startup.cs file, we can enable HealthChecks
and HealthChecksUI
by adding the following code below:
app.UseHealthChecks("/selfcheck", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).UseHealthChecksUI();
The "/selfcheck"
is the endpoint that you can call or your uptime monitoring would call for getting the status of the app. For example, if you run the app and navigate to "/selfcheck"
, it should return you the following response in JSON
format:
{
"status": "Unhealthy",
"totalDuration": "00:00:02.0427058",
"entries": {
"Google Ping": {
"data": {},
"description": "Ping check for www.google.com is ok.",
"duration": "00:00:00.0308662",
"status": "Healthy"
},
"Bing Ping": {
"data": {},
"description": "Ping check status [TimedOut].
Host www.bing.com did not respond within 100 ms.",
"duration": "00:00:00.4633644",
"status": "Unhealthy"
},
"Auth Server": {
"data": {},
"description": "No connection could be made because
the target machine actively refused it.",
"duration": "00:00:02.0204641",
"exception": "No connection could be made because
the target machine actively refused it.",
"status": "Degraded"
},
"External Api": {
"data": {},
"description": "No connection could be made because
the target machine actively refused it.",
"duration": "00:00:02.0219353",
"exception": "No connection could be made because
the target machine actively refused it.",
"status": "Degraded"
},
"PostgreSQL": {
"data": {},
"description": "Host can't be null",
"duration": "00:00:00.0083434",
"exception": "Host can't be null",
"status": "Unhealthy"
},
"SQL": {
"data": {},
"duration": "00:00:00.0246875",
"status": "Healthy"
}
}
}
And if you want to have a nice Visualization for monitoring the health status of each check, then you can simply navigate to "/healthchecks-iu"
and you should be presented with something like this:
That’s just awesome!
For more information about configuring ASP.NET Core HealthChecks, see https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks.
Secure Swagger Documentation with Bearer Authorization
In Swagger
, we can describe how we secure our APIs
by defining one or more security schemes. Since the template uses JWT
to protect the APIs, we can define a "Bearer"
scheme to protect our SwaggerUI
API documentation. In Infrastructure/Installers/RegisterSwagger.cs file, you can find the following code below:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{ Title = "ASP.NET Core Template API", Version = "v1" });
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Scheme = "Bearer",
Description = "Enter 'Bearer' following by space and JWT.",
Name = "Authorization",
Type = SecuritySchemeType.Http,
});
options.OperationFilter<SwaggerAuthorizeCheckOperationFilter>();
});
The code snippet above defines a security definition using "Bearer"
security scheme of type Http
. The SecuritySchemeType.Http
type is part of OpenApi 3
specification that is used for Basic
, Bearer
and other Http
authentications schemes.
One thing to notice in the code above is the OperationFilter
injection. That line enables applying security definition for APIs that requires Bearer
scheme. Here’s the code for SwaggerAuthorizeCheckOperationFilter.cs class that sits under Infrastructure/Filters folder:
internal class SwaggerAuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes
(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType
<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd
("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd
("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},new List<string>()
}
} };
}
}
API
endpoints that don’t require authorization will be ignored and the filter will only be applied based on the presence of the AuthorizeAttribute
.
Here's a sample screenshot of the secured SwaggerUI
:
Notice that the authorization is only applied to specific API
endpoints. In this case, only the SampleApi
controller is protected with the Bearer
scheme authorization. Clicking the Authorize
button or any of the SampleApi
endpoints should prompt you the following dialog:
Once you supply a valid access token, you should be able to test the secured endpoints from SwaggerUI
.
If you value performance, then you may want to implement pagination in your API to limit the amount of data to the requesting client. You may have a GET
endpoint that returns all the data to clients and when the data that your API serves grow, then your API
might end up being unusable.
Paging refers to getting partial results from an API
. Imagine having millions of results in the database and having your application try to return all of them at once.
Not only would that be an extremely ineffective way of returning the results, but it could also possibly have devastating effects on the application itself or the hardware it runs on. Moreover, every client has limited memory resources and it needs to restrict the number of shown results.
Pagination helps performance and scalability in a number of ways:
- The number of page read I/Os is reduced when SQL Server grabs the data.
- The amount of data transferred from the database server to the web server is reduced.
- The amount of memory used to store the data on the web server in our object model is reduced.
- The amount of data transferred from the web server to the client is reduced.
- This all adds up to potentially a significant positive impact – particularly for large collections of data.
You can find the paging example under Data/DataManager/PersonsManager.cs file. Here’s the code snippet:
public async Task<(IEnumerable<Person> Persons, Pagination Pagination)>
GetPersonsAsync(UrlQueryParameters urlQueryParameters)
{
IEnumerable<Person> persons;
int recordCount = 0;
var query = @"SELECT ID, FirstName, LastName FROM Person
ORDER BY ID DESC
OFFSET @Limit * (@Offset -1) ROWS
FETCH NEXT @Limit ROWS ONLY";
var param = new DynamicParameters();
param.Add("Limit", urlQueryParameters.PageSize);
param.Add("Offset", urlQueryParameters.PageNumber);
if (urlQueryParameters.IncludeCount)
{
query += " SELECT COUNT(ID) FROM Person";
var pagedRows = await DbQueryMultipleAsync<Person>(query, param);
persons = pagedRows.Data;
recordCount = pagedRows.RecordCount;
}
else
{
persons = await DbQueryAsync<Person>(query, param);
}
var metadata = new Pagination
{
PageNumber = urlQueryParameters.PageNumber,
PageSize = urlQueryParameters.PageSize,
TotalRecords = recordCount
};
return (persons, metadata);
}
The GetPersonsAsync()
takes a UrlQueryParameters
object and returns a named Tuple
called Persons
and Pagination
.
The code snippet above performs pagination based on the page numbers and page size supplied by the requesting client. However, there are cases that the client app also requires your API
to include the total number of records in the response so they can also present paginated data in the UI. Including the total record count can potentially impose performance penalty as we need to perform 2 SQL queries in the database: First is to get the chunk of data and second is to get the total record count.
This is the reason why we've added the IncludeCount
as part of the UrlQueryParameters
so that we can turn off this feature by default. When the client set IncludeCount = true
, we use the Dapper's QueryMultipleAsync()
method to perform multiple queries and reads multiple result set in a single database round trip.
You can find how the UrlQueryParameters
and Pagination
classes are defined under the Data folder. Here’s how the method GetPersonsAsync()
is being called in the PersonsController
class:
[Route("paged")]
[HttpGet]
public async Task<IEnumerable<PersonResponse>>
Get([FromQuery] UrlQueryParameters urlQueryParameters)
{
var data = await _personManager.GetPersonsAsync(urlQueryParameters);
var persons = _mapper.Map<IEnumerable<PersonResponse>>(data.Persons);
Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(data.Pagination));
return persons;
}
Now when you issue the following GET
request:
https://localhost:44321/api/v1/persons/paged?pagenumber=1&pagesize=3&includecount=true
The response output should return something like this:
{
"message": "Request successful.",
"isError": false,
"result": [
{
"id": 14002,
"firstName": "Vianne Maverich",
"lastName": "Durano",
"dateOfBirth": "2019-11-26T00:00:00",
"fullName": "Vianne Maverich Durano"
},
{
"id": 13002,
"firstName": "Vynn Markus",
"lastName": "Durano",
"dateOfBirth": "2019-11-26T00:00:00",
"fullName": "Vynn Markus Durano"
},
{
"id": 12002,
"firstName": "Michelle",
"lastName": "Durano",
"dateOfBirth": "1990-11-03T00:00:00",
"fullName": "Michelle Durano"
}
]
}
And the custom header X-Pagination
should be added in the response headers that contains the metadata
for paging as shown in the figure below:
The API
response uses AutoWrapper to automatically format the Http
response.
Unit Test Project
The template also includes a Unit Test project using xUnit
and Moq
. Here’s a sample code snippet of the test class:
public class PersonsControllerTests
{
private readonly Mock<IPersonManager> _mockDataManager;
private readonly PersonsController _controller;
public PersonsControllerTests()
{
var logger = Mock.Of<ILogger<PersonsController>>();
var mapperProfile = new MappingProfileConfiguration();
var configuration = new MapperConfiguration(cfg => cfg.AddProfile(mapperProfile));
var mapper = new Mapper(configuration);
_mockDataManager = new Mock<IPersonManager>();
_controller = new PersonsController(_mockDataManager.Object, mapper, logger);
}
private IEnumerable<Person> GetFakePersonLists()
{
return new List<Person>
{
new Person()
{
ID = 1,
FirstName = "Vynn Markus",
LastName = "Durano",
DateOfBirth = Convert.ToDateTime("01/15/2016")
},
new Person()
{
ID = 2,
FirstName = "Vianne Maverich",
LastName = "Durano",
DateOfBirth = Convert.ToDateTime("02/15/2016")
}
};
}
private CreatePersonRequest FakeCreateRequestObject()
{
return new CreatePersonRequest()
{
FirstName = "Vinz",
LastName = "Durano",
DateOfBirth = Convert.ToDateTime("02/15/2016")
};
}
private UpdatePersonRequest FakeUpdateRequestObject()
{
return new UpdatePersonRequest()
{
FirstName = "Vinz",
LastName = "Durano",
DateOfBirth = Convert.ToDateTime("02/15/2016")
};
}
private CreatePersonRequest FakeCreateRequestObjectWithMissingAttribute()
{
return new CreatePersonRequest()
{
FirstName = "Vinz",
LastName = "Durano"
};
}
private CreatePersonRequest FakeUpdateRequestObjectWithMissingAttribute()
{
return new CreatePersonRequest()
{
FirstName = "Vinz",
LastName = "Durano"
};
}
[Fact]
public async Task GET_All_RETURNS_OK()
{
_mockDataManager.Setup(manager => manager.GetAllAsync())
.ReturnsAsync(GetFakePersonLists());
var result = await _controller.Get();
var persons = Assert.IsType<List<PersonResponse>>(result);
Assert.Equal(2, persons.Count);
}
[Fact]
public async Task GET_ById_RETURNS_OK()
{
long id = 1;
_mockDataManager.Setup(manager => manager.GetByIdAsync(id))
.ReturnsAsync(GetFakePersonLists().Single(p => p.ID.Equals(id)));
var person = await _controller.Get(id);
Assert.IsType<PersonResponse>(person);
}
[Fact]
public async Task GET_ById_RETURNS_NOTFOUND()
{
var apiException = await Assert.ThrowsAsync<ApiException>(() => _controller.Get(10));
Assert.Equal(404, apiException.StatusCode);
}
}
The test project should include tests for POST
, PUT
and DELETE
methods. I’ve just trimmed down the test code for simplicity. Here’s the screenshot of the tests:
Sample Methods for StringExtensions
It also occurred to me that converting string
s to type datetime
, int
and long
are pretty much common when parsing data so I thought I would include a few sample methods to handle that. Here are the StringExtension
methods:
namespace ApiBoilerPlate.Infrastructure.Extensions
{
public static class StringExtensions
{
public static DateTime ToDateTime(this string dateString)
{
DateTime resultDate;
if (DateTime.TryParse(dateString, out resultDate))
return resultDate;
return default;
}
public static DateTime? ToNullableDateTime(this string dateString)
{
if (string.IsNullOrEmpty((dateString ?? "").Trim()))
return null;
DateTime resultDate;
if (DateTime.TryParse(dateString, out resultDate))
return resultDate;
return null;
}
public static int ToInt32(this string value, int defaultIntValue = 0)
{
int parsedInt;
if (int.TryParse(value, out parsedInt))
{
return parsedInt;
}
return defaultIntValue;
}
public static int? ToNullableInt32(this string value)
{
if (string.IsNullOrEmpty(value))
return null;
return value.ToInt32();
}
public static long ToInt64(this string value, long defaultInt64Value = 0)
{
long parsedInt64;
if (Int64.TryParse(value, out parsedInt64))
{
return parsedInt64;
}
return defaultInt64Value;
}
public static long? ToNullableInt64(this string value)
{
if (string.IsNullOrEmpty(value))
return null;
return value.ToInt64();
}
}
}
That’s it!. Feel free to request an issue on Github if you find bugs or request a new feature. Your valuable feedback is much appreciated to better improve this project. If you find this useful, please give it a star to show your support for this project. Thank you!
References
History
- 1st December, 2019: Initial version