ProudMonkey

ApiBoilerPlate: New Features and Improvements for Building ASP.NET Core 3 APIs

This space is for rent. Contact me at vmsdurano at gmail dot com.

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 recently been released. 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.

Tools and Frameworks Used

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.
  • 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 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 request 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)
    {
        // request the access token
        var accessToken = await _authServerConnect.RequestClientCredentialsTokenAsync();

        // set the bearer token to the outgoing request as Authentication Header
        request.SetBearerToken(accessToken);

        // Proceed calling the inner handler, that will actually send the requestto our protected api
        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 below 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 set 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 go beyond a certain threshold, in this case a 5 seconds 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 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 under Infrastructure/Installer/RegisterRequestRateLimiter.cs file. Here’s the code snippet:

internal class RegisterRequestRateLimiter : IServiceRegistration  
{
    public void RegisterAppServices(IServiceCollection services, IConfiguration config)
    {
        // needed to load configuration from appsettings.json
        services.AddOptions();
        // needed to store rate limit counters and ip rules
        services.AddMemoryCache();

        //load general configuration from appsettings.json
        services.Configure<IpRateLimitOptions>(config.GetSection("IpRateLimiting"));

        // inject counter and rules stores
        services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
        services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();

        // https://github.com/aspnet/Hosting/issues/793
        // the IHttpContextAccessor service is not registered by default.
        // the clientId/clientIp resolvers use it.
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        // configuration (resolvers, counter key builders)
        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 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:

//Register HealthChecks and UI
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:

//Enable HealthChecks and UI
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 checks, 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:

public class SwaggerAuthorizeCheckOperationFilter : IOperationFilter  
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // Check for authorize attribute
        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 are 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.

Sample Code for API Pagination

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: 1st is to get the chunk of data and 2nd 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()
    {

        // Arrange
        _mockDataManager.Setup(manager => manager.GetAllAsync())
           .ReturnsAsync(GetFakePersonLists());

        // Act
        var result = await _controller.Get();

        // Assert
        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 strings to type datetime, int and long are pretty much common when parsing data so I though I would include a few sample methods to handle that. Here's 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

Buy Me A Coffee