ASP.NET Core API
Create Web APIs using ASP.NET Core
Minimal API
ASP.NET Core Minimal API is a low footprint and super mini sized project type helps to develop an API with less code
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Project
Create a web API project with Swagger support (ASP.NET Core 5+):
bashdotnet new webapi -n [project name]
The above command scaffolds these files and folder (Check Core Project Structure):
Controller/
Properties/launchSettings.json
: Visual Studio will use the profiles defined here to run the projectappsettings.json
: Application configuration file, it store details such as database connection strings, API keys, etc.[project name].csproj
: Defines how the project will be builtProgram.cs
: Entry point of the .NET Core applications. Host configuration.Startup.cs
: Configures services and application request pipelines (middle-ware)public IConfiguration Configuration { get; }
: Read configuration settings from multiple sources likeappsettings.json
, Environment variables, files...
Trust the development certificate provided by dotnet:
bashdotnet dev-certs https --trust
Build and run the project.
bashdotnet run
Open Swagger page:
https://localhost:5001/swagger
Routing
Attribute routing is preferred with REST APIs.
Configure routing in your API application:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Matches request to an endpoint.
app.UseRouting();
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
// Adds endpoints for controller actions to the `IEndpointRouteBuilder` without specifying any routes.
endpoints.MapControllers();
});
}
- Middlewares that depend on endpoint resolution such as
app.UseAuthentication()
,app.UseAuthorization()
, etc. must be placed betweenUseRouting
andUseEndpoints
Register routing service:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(options =>
{
// Set all generated paths URLs to lowercase
options.LowercaseUrls = true;
});
}
Controllers
A controller based web API consists of one or more controller classes that derive from ControllerBase
.
ControllerBase
is lightweight and dose not have support for views.
Example:
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
// POST: api/WeatherForecast/pets
[HttpPost("pets", Name = nameof(Create))]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Pet>> AddPetsAsync([Frombody] Pet pet)
{
pet.Id = _petsInMemoryStore.Any() ?
_petsInMemoryStore.Max(p => p.Id) + 1 : 1;
await _petsInMemoryStore.AddAsync(pet);
return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
}
Register the Controller service:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
// Register filters here
options.Filters.Add<JsonExceptionFilter>();
// Use this if suffix "Async" should not be trimmed off of action names
// "public async IActionResult ListAsync()" will be matched
// with "List" not "ListAsync"
options.SuppressAsyncSuffixInActionNames = false;
});
}
ApiController Attribute
ApiController attribute: The [ApiController]
attribute can be applied to a controller class to enable the following opinionated, API-specific behaviours:
- Attribute routing is a requirement:
[Route("[controller]")]
- Automatic HTTP 400 responses: makes model validation errors automatically trigger an HTTP 400 response
- Binding source parameter inference
- Multipart/form-data request inference
- Problem details for error status codes
Some useful attributes:
[Route]
: Specifies URL pattern for a controller or action.[Bind]
: Specifies prefix and properties to include for model binding.[HttpGet]
: Identifies an action that supports the HTTP GET action verb.[Consumes]
: Specifies data types that an action accepts.[Produces]
: Specifies data types that an action returns.
Binding Source Parameter Inference
A binding source attribute defines the location at which an action parameter's value is found.
The following binding source attributes exist:
Attribute | Binding source |
---|---|
[FromBody] | Request body |
[FromForm] | Form data in the request body |
[FromHeader] | Request header |
[FromQuery] | Request query string parameter |
[FromRoute] | Route data from the current request |
[FromServices] | The request service injected as an action parameter |
[HttpGet]
public ActionResult<List<Product>> Get([FromQuery] bool discontinuedOnly = false)
The [ApiController] attribute applies inference rules for the default data sources of action parameters.
These rules save you from having to identify binding sources manually by applying attributes to the action parameters.
Control API Behaviour
- The default inference rules can be disabled by setting
SuppressInferBindingSourcesForParameters
totrue
Example:
services.AddControllers()
.ConfigureApibehaviourOptions(options =>
{
// Disable multipart/form-data request inference when [FromForm] is used
options.SuppressConsumesConstraintForFormFileParameters = true;
// Disable default inference rules
options.SuppressInferBindingSourcesForParameters = true;
// Disable automatic 400 response
options.SuppressModelStateInvalidFilter = true;
// Disable ProblemDetails response
options.SuppressMapClientErrors = true;
});
Return Types
Specific Type: Return the data with no HttpStatus code
IActionResult
: Return data with HttpStatus codeActionResult<T>
: Implements IActionResult and extends the return type- No need to use
Type
property of[ProducesResponseType]
:[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
- No need to use
public ActionResult<Product> GetById(int id)
Base Controller Class
A custom base controller class can be created, which will be annotated with attributes that will be used on most of the controllers defined in the application such as [ApiController]
[ApiController]
public class MyControllerBase : ControllerBase
{
}
// Derived Controller
[Produces(MediaTypeNames.Application.Json)]
[Route("[controller]")]
public class PetsController : MyControllerBase
Exception Handling
An exception filter that catches any unhandled exceptions and send the error message back to the client as a nicely formatted JSON response
- Create a class to model exception response
- Create a filter: Filter runs before or after ASP.NET Core processes a request
You can use the new ProblemDetails
class to return error responses:
- It follows the RFC 9457 - Problem Details for HTTP APIs (2023) standard for problem details
// register problem details filter
app.AddProblemDetailsExceptionHandler(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance = $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);
};
});
// sample API endpoint
app.MapGet("/weather", asycn (string city, string units, OpenWeatherMapService weatherMapService) =>
{
if (["f", "k", "c"].Contains(units.ToLower()) == false)
{
return Results.Problem(type: "Bad Request", title: "Invalid units", statusCode: StatusCodes.Status400BadRequest, detail: "Units must be 'f', 'k', or 'c'");
}
return Results.Ok(await weatherMapService.GetWeatherAsync(city, units));
});
You can also create a global ProblemDetails filter:
// Program.cs
builder.Services.AddExceptionHandler<ProblemExceptionHandler>();
app.UseExceptionHandler();
// ProblemExceptionHandler.cs
namespace Problems;
[Serializable]
public class ProblemException : Exception
{
public string Error { get; }
public string Message { get; }
public ProblemException(string error, string message) : base(message)
{
Error = error;
Message = message;
}
}
public class ProblemExceptionHandler : IExceptionHandler
{
private readonly IProblemDetailsService _problemDetailsService;
public ProblemExceptionHandler(IProblemDetailsService problemDetailsService)
{
_problemDetailsService = problemDetailsService;
}
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
if (exception is not ProblemException problemException)
{
return true;
}
var problemDetails = new ProblemDetails
{
Title = problemException.Error,
Detail = problemException.Message,
Status = StatusCodes.Status400BadRequest,
Type = "https://tools.ietf.org/html/rfc7807", // "Bad Request"
};
httpContext.Response.StatusCode = problemDetails.Status.Value;
return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext{
HttpContext = httpContext,
ProblemDetails = problemDetails
});
}
}
// now the endpoint can be simplified
app.MapGet("/weather", asycn (string city, string units, OpenWeatherMapService weatherMapService) =>
{
if (["f", "k", "c"].Contains(units.ToLower()) == false)
{
throw new ProblemException("Invalid Units", "Units must be 'f', 'k', or 'c'");
}
return Results.Ok(await weatherMapService.GetWeatherAsync(city, units));
});
Resources
RESTful APIs are all about returning and manipulating resources.
- We can use a base class for all the resource models we'll need to return from the API.
Entities / Domains / Models
A model is a set of classes that represent the data that the app manages.
Create record
Types instead of class
as they provide more features like:
- Use for immutable objects
- With-expressions support
- Value-based equality support
Using init
instead of set
or private set
:
// You can do this
Item item = new()
{
Id = Guid.NewGuid()
};
// But not this
item.Id = Guid.NewGuid();
Example folder structures:
Data/Models/[modelClasses].cs
Entity Framework lets us use simple classes to represent database entities.
- These are sometimes called POCOs (Plain Old CLR Objects)
Asynchronous
Asynchronous all the way.
Database
Working with Database using:
- Entity Framework Core
- Dapper
Authentication And Authorization
Implemented using the ASP.NET Core Identity packages.
ASP.NET Core Identity contains reusable code for dealing with accounts, passwords, and other authentication concerns.
Use
OpenIddict
for OpenId implementation
API Security
Checkout Application Security.
Transport security: Add security settings to Reverse Proxies as well
Turn oh HTTPS (enabled by default)
Reject HTTP, by using HTTP Strict Transport Security (HSTS):
Useful for browsers, but an edge case for API projects
Do not use
[RequireHttpsAttribute]
in API projectsDon't redirect
app.UseHttpsRedirection()
, instead APIs should either:- Not listen on HTTP
- Close the connection with status code 400 (Bad Request) and not serve the request
Application security
Cross-Origin Resource Sharing (CORS)
Dose your API need CORS?
- Accessed by browsers
Server can whitelist certain origins, HTTP methods, headers, and other element of the request.
Add CORS middleware:
csvar MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; // Create CORS policy services.AddCors(options => { options.AddPolicy(name: MyAllowSpecificOrigins, builder => builder.WithOrigins("https://example.com")); // can be used in development // .AllowAnyOrigin() // More refined control // WithOrigins("https://25prabhu10.github.io", "https://example.com").WithMethods("POST"); }); app.UseRouting(); // Add CORS middleware app.UseCors(MyAllowSpecificOrigins);
Health Check
Health Checks allow us to determine the overall health and availability of our application infrastructure
Application exposes HTTP endpoints which can be configured to provide information:
- Response time
- Memory usage
ASP.NET provides 3 different health check levels:
Healthy: Application is healthy and in a normal, working state.
Unhealthy: Application is unhealthy and is offline or an unhandled exception was thrown while executing the check.
Degraded: Application is still running, but not responding within an expected timeframe.
Types of Health Checks:
Basic health probes are the simplest form of health checking. They are configured as a URL endpoint, which will respond with a health response. These basic checks allow us to very quickly determine if our application is healthy or not.
System health checks give us a plethora of information, such as disk storage and memory usage of the underlying host our application is running on.
Database probes can determine whether our database provider is online and whether our application can successfully talk to it.
Custom health checks can range from anything such as checking a 3rd party service or API our application relies on, to checking our logging storage is not near capacity. These are more general and would require custom code to be written to execute the checks.
Example:
// Health checks can be chained
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>("example_health_check")
.AddSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health")
.RequireHost("www.contoso.com:5001")
.RequireAuthorization();
});
To check health hit route:
/health
To make health check depend on the health of dependents (databases) use the appropriate Nuget package, for mongodb:
bashdotnet add package AspNetCore.HealthChecks.MongoDb
Create custom health checks:
// Create a new health check service
public class ExampleHealthCheck: IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var healthCheckResultHealthy = true;
if (healthCheckResultHealthy)
{
return Task.FromResult(
HealthCheckResult.Healthy("A healthy result."));
}
return Task.FromResult(
new HealthCheckResult(context.Registration.FailureStatus,
"An unhealthy result."));
}
}
// Register the health check service
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>("example_health_check");
ASP.NET provides health check UI dashboard through the nuget package AspNetCore.HealthChecks.UI
:
Example:
// Configuration
{
"HealthChecks-UI": {
"HealthChecks": [
{
"Name": "Ordering HTTP Check",
"Uri": "http://host.docker.internal:5102/hc"
},
{
"Name": "Ordering HTTP Background Check",
"Uri": "http://host.docker.internal:5111/hc"
}
//...
]
}
}
// Startup.cs from WebStatus(Watch Dog) service
//
public void ConfigureServices(IServiceCollection services)
{
//…
// Registers required services for health checks
services.AddHealthChecksUI();
}
//…
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//…
app.UseHealthChecksUI(config => config.UIPath = "/hc-ui");
//…
}
Secrets Management
API Versioning
APIs need to be versioned if breaking changes need to be implemented
Create directories/namespaces like
v1
,v2
and add the controllers inside themUse
Microsoft.AspNetCore.Mvc.Versioning
package:services.AddApiVersioning()
Approaches to Restful Versioning:
Don't version (clients should be dynamic)
Content/Media type (header) versioning: Content negotiation process that already exists in HTTP. When clients request a media type using the Accept header they can explicitly include a version number or string in the media type itself.
httpGET api/test HTTP/1.1 host: localhost Accept: application/ion+json;v=2.0 content-type: text/plain;v=1.0
csservices.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; options.ApiVersionReader = new MediaTypeApiVersionReader(); // Send API version info in response // api-supported-versions 1.0 options.ReportApiVersions = true; // Select most recent API version if there is no version defined as a part of the content-type media type options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options); });
URL prefix (path) versioning:
/api/v1/test
,/api/v2/test
- Least restful
- But more explicit to the clients
cs[ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiversion}/[controller]")] public class TestController : ControllerBase
Query String:
/api/test?api-version=1.0
,/api/test?api-version=2.0
cs[ApiVersion("1.0")] [ApiController] [Route("api/[controller]")] public class TestController : ControllerBase
- Set default version:
csservices.AddApiVersioning(config => { config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true; });
HTTP Header:
httpGET api/test HTTP/1.1 host: localhost custom-version-header: 1.0
csservices.AddApiVersioning(config => { config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true; config.ApiVersionReader = new HeaderApiVersionReader("custom-version-header"); });
API Tools
- Postman
- Swagger
Swagger setup with Swashbuckle and NSwag:
- Add middleware
- Add API details
- Add XML documentation
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "London API",
Version = "v1",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
// Add Middleware
if (env.IsDevelopment())
{
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.)
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "London API v1"));
}
// In controller/actions add doc string/comments
/// <summary>
/// Get response
/// </summary>
/// <returns>A String</returns>
/// <response code="200">Returns a string</response>
[HttpGet()]
public IActionResult GetRoot()
Caching Response
Client-side caching:
ASP.NET Core uses the
ResponseCache
attribute to indicate that a particular response is cacheable.cspublic class RootController : ControllerBase { [HttpGet(Name = nameof(GetRoot))] [ResposeCache(Duration = 86400)] public IActionResult GetRoot() { return Ok("Hello, World!"); } }
Response will include
Cache-Control
header:Cache-Control: public,max-age=86400
Using cache profiles we can reuse the cache settings in many places:
csservices.AddMvc(options => { options.CacheProfiles.Add("Static", new CacheProfile { Duration = 86400 }); }); // Add that profile to endpoints [HttpGet(Name = nameof(GetRoot))] [ResposeCache(CacheProfilename = "Static")] public IActionResult GetRoot() { return Ok("Hello, World!"); }
ASP.NET Core doesn't handle the
ETag
header by default
Server-side caching:
Add middleware:
services.AddResponseCaching()
andapp.UseResponseCaching()
Add the Response cache attribute to the endpoints:
cs[HttpGet(Name = nameof(GetRoot))] [ResposeCache(Duration = 86400, VeryByQeryKeys = new[] { "offset", "limit", "orderBy", "search" })] public IActionResult GetRoot() { return Ok("Hello, World!"); }
In-Memory Caching
services.AddMemoryCache();
- Add
Microsoft.Extensions.Caching.Memory
package
// Use DI to inject the memoryCache into services that need it
public SampleDataAccess(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public async Task<List<EmployeeModel>> GetEmployeesCache()
{
List<EmployeeModel> output;
output = _memoryCache.Get<List<EmployeeModel>>("employees");
if (output is null)
{
// Call DB
output.Add(new EmployeeModel {...});
// Add the response to cache
_memoryCache.Set("employees", output, TimeSpan.FromMinutes(1));
}
return output;
}
Docker
docker run -d --rm --name mongo -p 27017:27017 -v mongodbdata:/data/db mongo
Enable username and password:
docker run -d --rm --name mongo -p 27017:27017 -v mongodbdata:/data/db -e MONGO_INITDB_ROOT_USERNAME=mongoadmin -e MONGO_INITDB_ROOT_PASSWORD=Pass#word1 mongo