The CoreEx.AspNetCore
namespace provides extended capabilities to build Web APIs, for the likes of ASP.NET or HTTP-triggered Azure functions. The WebApi
and WebApiPublisher
capabilities (within the CoreEx.AspNetCore.WebApis
namespace) encapsulate the consistent handling of the HTTP request and corresponding response, whilst also providing additional capabilities that are not available out-of-the-box within the .NET runtime.
To standardize, and simplify, the development of JSON-based Web APIs. The key integration patterns currently being addressed are as follows:
Pattern | Description | Capability |
---|---|---|
Request-response | This represents a real-time request-response, whereby the request is immediately fulfilled (synchronous) with the response representing the result of the request. | WebApi |
Fire-and-forget | This is to enable decoupled asynchronous processing, whereby the request is immediately accepted (queued internally), with a separate internal process that fulfils the request independently of the request. | WebApiPublish |
Only JSON-based Web APIs are generally supported. Where additional or other content types are needed then this library in its current state will not be able to enable, and these Web APIs will need to be implemented in a traditional custom manner.
There is provision such that any result of type IActionResult
, for example FileContentResult
, is returned these will be enacted by the ASP.NET Core runtime as-is (i.e. no CoreEx.AspNetCore
processing will occur on the result). However, all other request handling, exception handling, logging, etc. described below will occur which has a consistency benefit.
The WebApi
class should be leveraged as the primary means to enable Web API functionality, it provides methods for HTTP GET
, POST
, PUT
, PATCH
and DELETE
operations that encapsulates the execution in a standardized manner, providing alternate overloads and options to enable the desired behaviours.
The WebApi
extends (inherits) WebApiBase
that provides the base RunAsync
method that all other methods invoke to wrap the underlying logic. This in turns invokes the WebApiInvoker
which provides a pluggable mechanism (i.e. can be replaced) that by default handles the following consistently for each request:
- Infers the standard
WebApiRequestOptions
from the HTTP request headers and query string (names are configurable). - Infers the correlation identifier from the HTTP request header (names are configurable).
- Begins a logging scope to include the correlation identifier.
- Invokes the request logic and returns the corresponding
IActionResult
. - Handle all exceptions:
- Where the exception implements
IExtendedException
then returnsIExtendedException.ToResult()
. Also, whereIExtendedException.ShouldBeLogged
istrue
then aILogger.LogError
will occur; some errors, such as400-BadRequest
, need not be logged as they are not a run-time error per se. - Invoke the protected
OnUnhandledExceptionAsync
then return resultingIActionResult
where notnull
.
- Where the exception implements
WebApi
provides the following per HTTP method; each with varying overloads depending on need. Where a generic Type
is specified, either TValue
being the request content body and/or TResult
being the response body, this signifies that WebApi
will manage the underlying JSON serialization:
HTTP | Method | Description |
---|---|---|
GET |
GetAsync<TResult>() |
Performs a GET operation. |
POST |
PostAsync() PostAsync<TValue>() PostAsync<TResult>() Post<TValue, TResult>() |
Performs a POST operation. |
PUT |
PutAsync<TValue>() PutAsync<TValue, TResult>() |
Performs a PUT operation. |
PATCH |
PatchAsync<TValue> |
Performs a PATCH operation. Support for application/merge-patch+json with JsonMergePatch . |
DELETE |
DeleteAsync() |
Performs a DELETE operation. |
* |
RunAsync() RunAsync<TValue>() |
Performs any operation returning an IActionResult . |
Where a request contains a content body that contains JSON (content-type of application/json
) then these methods can (where the TValue
is defined) perform the deserialization using the appropriate IJsonSerailizer
. The corresponding WebApiRequestOptions
are also automatically inferred as described above.
Where using CoreEx
to perform the JSON deserialization then the value is not specified as an argument within the method (typically with the FromBody
attribute). However, this will mean that the value type is not output when Swagger output is generated; to enable, use the AcceptsBody
attribute to specify.
Where a TResult
value is returned then these methods will perform the JSON serialization, using the appropriate IJsonSerailizer
. This is managed by the underlying ValueContentResult.CreateResult
which additionally performs the following:
Step | Description |
---|---|
PagingResult headers |
Where response value is ICollectionResult then sets PagingResult headers and returns underlying collection (ICollectionResult.Collection ). |
JSON serialization | Serializes the TResult value using the IJsonSerailizer . Where include or exclude fields were specified within the request query string then these will be applied (IJsonSerializer.TryApplyFilter ) to the JSON response to limit the response content. |
ETag generation |
Checks if value implements IETag , where non-null leave as-is; otherwise, automatically generate ETag hash from serialized value (excluding filters). |
GET If-Match |
Where the value/generated ETag equals the GET request If-Match value then return an HTTP status code of 304-NotModified with no content. |
ETag header |
Sets the HTTP ETag header using either IETag.ETag or generated hash. |
Status code | Sets the response HTTP status code as configured. |
Location |
Sets the HTTP Location header where specified (where applicable). |
As described earlier, the above will not occur for IActionResult
results.
The following demonstrates usage when creating an ASP.NET Controller
:
[Route("api/employees")]
public class EmployeeController : ControllerBase
{
private readonly WebApi _webApi;
private readonly EmployeeService _service;
public EmployeeController(WebApi webApi, EmployeeService service)
{
_webApi = webApi;
_service = service;
}
[HttpGet("{id}", Name = "Get")]
public Task<IActionResult> GetAsync(Guid id)
=> _webApi.GetAsync(Request, _ => _service.GetEmployeeAsync(id));
[HttpGet("", Name = "GetAll")]
public Task<IActionResult> GetAllAsync()
=> _webApi.GetAsync(Request, p => _service.GetAllAsync(p.RequestOptions.Paging));
[HttpPost("", Name = "Create")]
public Task<IActionResult> CreateAsync()
=> _webApi.PostAsync<Employee, Employee>(Request, p => _service.AddEmployeeAsync(p.Validate<Employee, EmployeeValidator>()),
statusCode: HttpStatusCode.Created, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute));
[HttpPut("{id}", Name = "Update")]
public Task<IActionResult> UpdateAsync(Guid id)
=> _webApi.PutAsync<Employee, Employee>(Request, p => _service.UpdateEmployeeAsync(p.Validate<Employee, EmployeeValidator>(), id));
[HttpPatch("{id}", Name = "Patch")]
public Task<IActionResult> PatchAsync(Guid id)
=> _webApi.PatchAsync(Request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Validate<Employee, EmployeeValidator>(), id));
[HttpDelete("{id}", Name = "Delete")]
public Task<IActionResult> DeleteAsync(Guid id)
=> _webApi.DeleteAsync(Request, _ => _service.DeleteEmployeeAsync(id));
The following demonstrates usage when creating an Azure HTTP-triggered Function (essentially the same _webApi
invocation code to Controller
above):
public class EmployeeFunction
{
private readonly WebApi _webApi;
private readonly EmployeeService _service;
public EmployeeFunction(WebApi webApi, EmployeeService service)
{
_webApi = webApi;
_service = service;
}
[FunctionName("Get")]
public Task<IActionResult> GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees/{id}")] HttpRequest request, Guid id)
=> _webApi.GetAsync(request, _ => _service.GetEmployeeAsync(id));
[FunctionName("GetAll")]
public Task<IActionResult> GetAllAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/employees")] HttpRequest request)
=> _webApi.GetAsync(request, p => _service.GetAllAsync(p.RequestOptions.Paging));
[FunctionName("Create")]
public Task<IActionResult> CreateAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/employees")] HttpRequest request)
=> _webApi.PostAsync<Employee, Employee>(request, p => _service.AddEmployeeAsync(p.Validate<Employee, EmployeeValidator>()),
statusCode: HttpStatusCode.Created, locationUri: e => new Uri($"api/employees/{e.Id}", UriKind.RelativeOrAbsolute));
[FunctionName("Update")]
public Task<IActionResult> UpdateAsync([HttpTrigger(AuthorizationLevel.Function, "put", Route = "api/employees/{id}")] HttpRequest request, Guid id)
=> _webApi.PutAsync<Employee, Employee>(request, p => _service.UpdateEmployeeAsync(p.Validate<Employee, EmployeeValidator>(), id));
[FunctionName("Patch")]
public Task<IActionResult> PatchAsync([HttpTrigger(AuthorizationLevel.Function, "patch", Route = "api/employees/{id}")] HttpRequest request, Guid id)
=> _webApi.PatchAsync(request, get: _ => _service.GetEmployeeAsync(id), put: p => _service.UpdateEmployeeAsync(p.Validate<Employee, EmployeeValidator>(), id));
[FunctionName("Delete")]
public Task<IActionResult> DeleteAsync([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "api/employees/{id}")] HttpRequest request, Guid id)
=> _webApi.DeleteAsync(request, _ => _service.DeleteEmployeeAsync(id));
The WebApiPublisher
class should be leveraged for fire-and-forget style APIs, where the message is received, validated and then published as an event for out-of-process decoupled processing.
The WebApiPublish
extends (inherits) WebApiBase
that provides the base RunAsync
method described above.
The WebApiPublisher
constructor takes an IEventPublisher
that is responsible for formatting and sending the event to the requisite messaging platform. See Events for more information regarding events.
A publish should be performed using an HTTP POST
and as such this is the only HTTP method supported. The WebApiPublish
provides the following overloads depending on need.
HTTP | Method | Description |
---|---|---|
POST |
PublishAsync<TValue>() |
Publish a single message/event with TValue being the request content body. |
POST |
PublishValueAsync<TValue>() |
Publish a single message/event with TValue being the specified value (preivously deserialized). |
POST |
PublishAsync<TValue, TEventValue>() |
Publish a single message/event with TValue being the request content body mapping to the specified event value type. |
POST |
PublishValueAsync<TValue, TEventValue>() |
Publish a single message/event with TValue being the specified value (preivously deserialized) mapping to the specified event value type. |
- | -
POST
|PublishCollectionAsync<TColl, TItem>()
| Publish zero or more message/event(s) from theTColl
collection with an item type ofTItem
being the request content body.POST
|PublishCollectionValueAsync<TColl, TItem>()
| Publish zero or more message/event(s) from theTColl
collection with an item type ofTItem
being the specified value (preivously deserialized).POST
|PublishCollectionAsync<TColl, TItem, TEventItem>()
| Publish zero or more message/event(s) from theTColl
collection with an item type ofTItem
being the request content body mapping to the specified event value type.POST
|PublishCollectionValueAsync<TColl, TItem, TEventItem>()
| Publish zero or more message/event(s) from theTColl
collection with an item type ofTItem
being the specified value (preivously deserialized) mapping to the specified event value type.
Depending on the overload used (as defined above), an optional argument can be specified that provides additional opportunities to configure and add additional logic into the underlying publishing orchestration.
The following argurment types are supported:
WebApiPublisherArgs<TValue>
- single message with no mapping.WebApiPublisherArgs<TValue, TEventValue>
- single message with mapping.WebApiPublisherCollectionArgs<TColl, TItem>
- collection of messages with no mapping.WebApiPublisherCollectionArgs<TColl, TItem, TEventItem>
- collection of messages with mapping.
The arguments will have the following properties depending on the supported functionality. The sequence defines the order in which each of the properties is enacted (orchestrated) internally. Where a failure or exception occurs then the execution will be aborted and the corresponding IActionResult
returned (including the likes of logging etc. where applicable).
Property | Description | Sequence
-|-
EventName
| The event destintion name (e.g. Queue or Topic name) where applicable. | N/A
StatusCode
| The resulting status code where successful. Defaults to 204-Accepted
. | N/A
OperationType
| The OperationType
. Defaults to OperationType.Unspecified
. | N/A
MaxCollectionSize
| The maximum collection size allowed/supported. | 1
OnBeforeValidateAsync
| The function to be invoked before the request value is validated; opportunity to modify contents. | 2
Validator
| The IValidator<T>
to validate the request value. | 3
OnBeforeEventAsync
| The function to be invoked after validation / before event; opportunity to modify contents. | 4
Mapper
| The IMapper<TSource, TDestination>
override. | 5
OnEvent
| The action to be invoked once converted to an EventData
; opportunity to modify contents. | 6
CreateSuccessResult
| The function to be invoked to create/override the success IActionResult
. | 7
A request body is mandatory and must be serialized JSON as per the specified generic types.
The response HTTP status code is 204-Accepted
(default) with no content. This can be overridden using the arguments StatusCode
property.
The following demonstrates usage when creating an Azure HTTP-triggered Function:
public class HttpTriggerQueueVerificationFunction
{
private readonly WebApiPublisher _webApiPublisher;
private readonly HrSettings _settings;
public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, HrSettings settings)
{
_webApiPublisher = webApiPublisher;
_settings = settings;
}
public Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request)
=> _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs<EmployeeVerificationRequest>(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() });
}