-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Contains common classes, resources and references used by all the API projects.
Contains entity framework core models, configuration and migrations.
Contains the CQRS commands used to update the database. To make a new command you will need to implement these:
- ICommand: used to pass arguments to the command validator and command handler
- AbstractCommandValidator: used to validate the arguments
- ICommandHandler: used to execute the database change
Here's an example command for adding a new classification to a database.
public class AddClassificationCommand : ICommand
{
public string Code { get; set; }
public string DescEng { get; set; }
public string DescFre { get; set; }
}
public class AddClassificationCommandValidator : AbstractCommandValidator<AddClassificationCommand>
{
public AddClassificationCommandValidator(ExampleDbContext db)
{
RuleFor(e => e.Code)
.NotEmpty()
.Must(e => !db.Classifications.Any(c => c.Code == e)).WithLocalizedStringMessage(typeof(Core.Resources.Validation), "CodeAlreadyExists")
;
RuleFor(e => e.DescEng)
.MaximumLength(250);
RuleFor(e => e.DescFre)
.MaximumLength(255);
}
}
public class AddClassificationCommandHandler : ICommandHandler<AddClassificationCommand>
{
private readonly ExampleDbContext _db;
public AddClassificationCommandHandler(ExampleDbContext db)
{
_db = db;
}
public async Task ExecuteAsync(AddClassificationCommand command, CancellationToken cancellationToken = new CancellationToken())
{
await _db.Classifications.AddAsync(new Classification()
{
Code = command.Code,
DescEng = command.DescEng,
DescFre = command.DescFre
}, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
}
}
Some key notes:
- All validation rules happen in the command validator.
- The command handler only executes if the command validator executed successfully.
- Commands do not return results. They either execute successfully or throw an exception.
- Any new database operation will require a new command to be written and involves creating these three classes: a command, a validator, and a handler.
Contains the CQRS queries used to read from the database. To make a query you need to make classes that implement:
- IQuery: used to pass arguments to the query handler
- IQueryHandler: used to return results from the database
Here's an example for returning a specific classification from the database:
public class GetClassificationByIdQuery : IQuery<ClassificationDto>
{
public Guid Id { get; set; }
}
public class GetClassificationByIdQueryHandler : IQueryHandler<GetClassificationByIdQuery, ClassificationDto>
{
private readonly ExampleDbContext _db;
public GetClassificationByIdQueryHandler(ExampleDbContext db)
{
_db = db;
}
public Task<ClassificationDto> HandleAsync(GetClassificationByIdQuery query, CancellationToken cancellationToken = new CancellationToken())
{
return _db.Classifications.Where(e => e.Id == query.Id)
.Select(e => new ClassificationDto()
{
Id = e.Id,
Code = e.Code,
DescEng = e.DescEng,
DescFre = e.DescFre
}).SingleOrDefaultAsync(cancellationToken);
}
}
In many cases you won't need to take in arguments. In those cases, there's a different IQueryHandler interface you can use. Here's an example returning all the classifications from the database:
public class GetAllClassificationsQueryHandler : IQueryHandler<List<ClassificationDto>>
{
private readonly ExampleDbContext _db;
public GetAllClassificationsQueryHandler(ExampleDbContext db)
{
_db = db;
}
public Task<List<ClassificationDto>> HandleAsync(CancellationToken cancellationToken = new CancellationToken())
{
return _db.Classifications
.Select(e => new ClassificationDto()
{
Id = e.Id,
Code = e.Code,
DescEng = e.DescEng,
DescFre = e.DescFre
}).ToListAsync(cancellationToken);
}
}
Contains the API Controllers and startup configuration.
API Controllers are used to expose the api endpoints.
- configuration is done with attributes
- Routing is controlled by attributes on the controller AND the action
- Actions should be as simple as possible. They're only responsible for authorizing access, executing queries/commands, and returning results.
To help execute a query or command you can ask for these objects are available with the dependency injector: ICommandSender and IQueryProcessor. Here's an example controller setup:
[ApiController, AllowAnonymous, Route("api/classification")]
public class ClassificationController : Controller
{
private readonly ICommandSender _commandSender;
private readonly IQueryProcessor _queryProvider;
public ClassificationController(IQueryProcessor queryProvider, ICommandSender commandSender)
{
_queryProvider = queryProvider;
_commandSender = commandSender;
}
ICommandSender is used to validate and execute a command. It will:
- validate the command
- if validation fails, it will send back error messages and a HTTP400 bad request
- if validation succeeds, it will execute the command handler and send back a HTTP200
Here's an example api action executing a command:
[HttpPost, Route("")]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
public async Task<IActionResult> Add([FromBody]AddClassificationCommand command)
{
return await _commandSender.ValidateAndSendAsync(command, ModelState);
}
IQueryProcessor is used to execute a query. There's two ways you can execute a query - with arguments or without. Here's examples of both
No arguments:
[HttpGet, Route("")]
[ProducesResponseType(typeof(List<ClassificationDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> Get()
{
var results =
await _queryProvider.ProcessAsync<GetAllClassificationsQueryHandler, List<ClassificationDto>>();
return Ok(results);
}
With arguments:
[HttpGet, Route("{Id}")]
[ProducesResponseType(typeof(ClassificationDto), StatusCodes.Status200OK)]
public async Task<IActionResult> GetById([FromRoute]GetClassificationByIdQuery query)
{
var results =
await _queryProvider.ProcessAsync(query);
return Ok(results);
}
The web project that runs an ASP.NET Core Web application used solely for the front-end presentation. It doesn't have dependencies on the other projects since all communication should be done with the API.
Project is setup to use Razor Pages (https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio).
Calls to the API can be done directly in a Razor Page action or by using a data service class. A data service class is a simple class to handle the API calls and can be useful to reduce complexity in the razor pages. Here's an example of a database service class and how it would be called in a Razor Page.
ClassificationService.cs
public class ClassificationService : IDataService
{
private readonly IHttpClientFactory _clientFactory;
public ClassificationService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<Classification[]> GetClassifications()
{
using var httpClient = _clientFactory.CreateClient("api");
return await httpClient.GetJsonAsync<Classification[]>("/api/classification");
}
}
Index.cshtml.cs
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly ClassificationService _classificationService;
public Classification[] Classifications;
public IndexModel(ILogger<IndexModel> logger, ClassificationService classificationService)
{
_logger = logger;
_classificationService = classificationService;
}
public async Task OnGet()
{
Classifications = await _classificationService.GetClassifications();
}
}