This post, was originally posted on kudocode.me and written by Marius Pruis, Senior Developer at Haefele Software. The purpose of the architecture is to modularise, isolate and control the flow of general components like authentication, authorization, validation, execution, error handling, basic auditing and responses using SOLID principles and clean code techniques. The architecture provides existing components as well as the ability to implement custom concrete components. It allows the developer to plug and play these components using the provided interfaces, abstractions and patterns. The architecture also controls the flow of the concrete components.
Below is an overview of the API architecture and each component highlighted in green as seen on the high-level sequence diagram below. These components (highlighted in green) are implemented by the developer utilizing the architecture in a normal development life cycle. The abstracted classes are provided by the framework and can be replaced for further customization.
High-level sequence diagram:
- Web API Controller
- Request DTO (TRequestDto)
- Result DTO (TOut)
- Concrete Authorization Handler
- Concrete Validation Handler
- Concrete Worker Handler
1. THE CONTROLLER
- IExecutionPipeline (Execution Pipeline)
- Request DTO (TRequestDto)
- Response DTO
The controller can be decorated with a TokenAuthenticationAttribute for authentication. The attribute extracts a JWT Authentication token from the request header to identify the user posting the request. A new Application User Context (IApplicationUserContext) is resolved from the IOC container and updated with the authenticated users information (groups and roles) for authorization. The implementation of the IApplicationUserContext is available to the concrete handlers for authorization and other use by injecting the interface into the constructors.
The controller method accepts a request DTO as a parameter. The object is referred to as a request DTO because it ultimately requests some action or result from the API and contains information the API needs to execute the request. The GetOne method in the example above accepts the request DTO GetLeadDto, implying we are requesting to fetch a single lead. We use objects because we can extend them with new properties without changing the signature of the method and preventing the risk parameters increasing.
The interface IExecutionPipeline injected into the constructor is responsible for managing the execution of the concrete handlers. The responsibility includes a single place to catch handled and unhandled exceptions as well as logging the exceptions and tracking the execution process. This removes the burden from concrete implementations. Exceptions thrown are caught in a single place, processed and added to the IApiResponseContextDto<TOut>.
The controller methods are thin to the degree that each method can be implemented using a single line of code. The line of code calls the Execute method on the IExecutionPipeline interface with two generic type parameters and the Request DTO. In the example above the first type parameter is the type of the request DTO (TRequestDto:GetLeadDto), the second type parameter is the type of the result expected back (TOut:LeadDto) and the last parameter is the request DTO passed into the controller method.
The execute method will return an implementation of IApiResponseContextDto where T is the expected result LeadDto (the second type parameter of the Execute function). The IApiResponseContextDto is a consistent response containing information whether a request was successful or unsuccessful. The response contains the Result (LeadDto) of type T and a list of Messages that contain any validation errors, errors or notifications.
2. THE CONCRETE HANDLERS
The three concrete handlers mentioned in developer components above, each implement a specific abstract handler that allows the Execution Pipeline to resolve the implementations and execute them on our behalf. Each handler has a single responsibility. Each of the three handler types has a context associated with it that is available to the developer through the property Context on the concrete handler. This context is initialized by the Execution Pipeline and passed into each handler using a context interface specific to the handler type. The interfaces control which properties are exposed to a specific handler. The context is initialized once and passed from one handler to the next, maintaining a single context throughout the execution. This allows us to separate the state maintained by the context from the implementation and abstraction. The IExecutionPipelineContext is used with the Execution Pipeline and inherits all other context interfaces.
The context interfaces are :
2.1 THE CONCRETE AUTHORIZATION HANDLER
- AbstractAuthorizationHandler<TRequestDto, TOut>
Implementing this abstract class enables us to associate authorization requirements to the TRequestDto and TOut requested from the controller. Multiple of these abstract handlers can be written to extend authorization to any combination of TRequestDto and TOut with different authorization requirements. In this example, we will use multiple groups and a single role per application user.
This abstract implementation requires an IAuthorizationHandlerDto implementation containing the list of groups and the single role we want to associate with this specific handler.
An application user can belong to multiple organizations and multiple groups within each organization. A group belongs to one organization. An application user has one role. A role belongs to one organization. The current application user should have at least one of the groups provided by the GetAuthorization method to allow the Execution Pipeline to continue. If this check fails the abstract handler will add an Authorization Failed error to the IAuthorizationContext and the Execution Pipeline will exit passing control back to the controller.
In this method implementation, we provide the abstract class the authorization requirements we want to associate with the <TRequestDto, TOut> combination and compare to the current application user. This method can source the requirements from config, code, Redis, or from the database by injecting the IReadOnlyRepository interface into the constructor.
We can implement the execute method if we choose to write custom checks that are not done by the abstract handler and add an Authorization Error to the IAuthorizationContext when the check fails. Adding an Authorization error E3 will cause the Execution Pipeline to exit and send the response back to the client. See implementation example above.
2.2 THE CONCRETE VALIDATION HANDLER
The validation handler uses a third party library Fluent Validation by implementing the AbstractValidator<T> class. The library allows for simple and complex validation scenarios using built-in validators and extensions for customizing validation (see the library documentation here).
Validation rules can be extracted into AbstractValidator<T> classes allowing for re-usability using the provided SetValidator function.
We can also use an extension method to extract groups of common validations with custom messages.
The concrete handler implementation is controlled by the FluentValidationHandler<TRequestDto, TOut> class which is provided by the framework. The FluentValidationHandler will collect all validation results on our behalf and add them to the IValidationContext<TOut> context. If any validation errors occur the Execution Pipeline will exit and pass the control back to the controller.
2.3 THE CONCRETE WORKER HANDLER
- Command/QueryHandler<TRequestDto, TEntity, TOut>
- WorkerHandler<TRequestDto, TOut>
Command/QueryHandler<TRequestDto, TEntity, TOut>
The QueryHandler seen in the example above exposes an IReadOnlyRepository interface through the constructor where the CommandHandler exposes an IRepository interface. The difference is the one limits the developer to only read from the repository and the other allows the developer to write. This helps to clarify the purpose and responsibility of the concrete handler. Separating these two concerns gives us the benefit of more flexible and maintainable objects. Most of the business logic is maintained in the concrete command handlers and the concrete query handlers can be relatively simple.
The Command/Query handler excepts the three generic type parameters we are interested in <TRequestDto, TEntity, TOut>. TEntity represents the database entity we are going to query or maintain in the concrete handler. TEntity (Lead) can represent a single entity or list of entities (see the example below).
TOut (LeadDto) is the result produced by the concrete handler. This is the result returned to the client through the IApiResponseContextDto<TOut> interface on the controller also the second generic type parameter of the Execute method on the interface IExecutionPipeline (see controller method GetOne).
The GetEntity method is the developer’s first entry point to the database through the repository. This is where we query the repository for the relevant TEntity. We assign the result of the repository query to the property Entity of type TEntity defined on the base Command/Query Handler making it available to the rest of the handler.
In a case where we create a new entity and a repository query if not required the Entity property will be initialized and mapped from the TRequestDto passed into the handler (see the example below).
This method isolates any checks we want to do against the database if required. We might want to check the result (the Entity property) from the GetEntity or perhaps check data from the TRequestDto against the database (E.g duplicate email address on registration ). In the example above we return an E4 error code indicating that the requested lead was not found. The Execution Pipeline will stop when the error is raised and pass control back to the controller.
The GetEntity and ValidateEntity methods allow us to separate queries from validation giving each implementation a single responsibility.
This is where we apply the business logic, conclude the transaction and compose the result TOut (LeadDto). In the GetLeadDtoWorkerHandler example above we map the entity to the type TOut and assign it to the Result property of the Context for the Execution Pipeline to process and pass the result to the controller.
In the case of a command (creating or updating a lead) we will conclude the transaction by calling the appropriate methods on the IRepository interface before we compose the result TOut (see the example below)
2.3.1 UTILIZING MULTIPLE HANDLERS
Consider a scenario where we want to utilize multiple handlers to accommodate a single business scenario. The business scenario states that a user will upload a document (CSV) containing a list of leads we want to import into the system. If parsing the document fails the process should exit and return any errors. If a lead already exists the system should generate a message saying the email is already in use and continue processing. The system should return a list of IDs for the created leads, including the error messages for each lead that already exists, to the client.
The code above uses the base handler WorkerHandler<TRequestDto, TOut>. The difference between this base handler and the previous Query/Command Handler is that there is no TEntity generic type associated with this handler because in this scenario the handler is not responsible for any specific entity or entities. The handlers reused in this handler is responsible for the entities associated with them.
The interface IExecutionPipeline is injected into the constructor. The Execution Pipeline is used in the Execute method to call the handlers we want to use or reuse in this scenario. This works the same as with the controller.
The first call to the Execution Pipeline calls for a handler associated with TRequestDto of type CreateLeadsFromCsvDto and TOut of type List<CreateLeadDto>. This handler is responsible for extracting data from the provided document in CreateLeadsFromCsvDto and transform the data into the result List<CreateLeadDto>. AttachResult(Context) will attach any messages returned to the current context to ensure all messages are passed back to the client. If the transformation was unsuccessful we call the return to end the process and pass control back to the controller.
Next, we loop the result List<CreateLeadDto> and pass each item to the handler associated with TRequestDto of type CreateLeadDto and TOut of type int. This is the same handler seen in the controller method Create. Each result is added to the current context and when complete the final result is passed back to the controller.
The Lead entity implements the interface IEntityAudit containing fields for basic auditing. When the Create and Update methods on the repository are invoked from the concrete handlers the repository checks if the entity is of type IEntityAudit. If the check passes the repository uses the Application User Context (IApplicationUserContext), constructed in the TokenAuthenticationAttribute mentioned in the controller section, to populate the appropriate fields on behalf of the developer reducing the responsibility of the concrete handlers.
This interface works the same as the IEntityAudit. The Application User fields are implemented on the entity and managed by the repository. Entities implementing this interface will only be accessible to the Application User responsible for creating them. When a query is done using the IReadOnlyRepository interface the concrete implementation will check if the entity queried is of type IBelongToApplicationUser. If the check holds true the repository will add a SQL filter to only return records that belong to the current user using the Application User Context.
Previously mentioned an Application user belongs to organizations, groups and a role. The entity can be extended with interfaces IBelongToOrganization, IBelongToAuthorizationGroup and IBelongToRole to accomplish the same goal as IBelongToApplicationUser
- ASP.Net Core
- EntityFramework Core
- Fluent Validation