-
Notifications
You must be signed in to change notification settings - Fork 0
Backend API
There are two starting points when working with MuffiNet:
- Standalone Service (Api.Standalone).
- With a React frontend (Api.WithReact).
The projects reference a shared project (Api.Shared). The goal is to keep the API-projects thin and with the most of the code in the Domain Model-project.
The principles behind the Domain Model is taken from Domain-Driven Design (DDD) and partly from Event-driven Architecture (EDA).
The Domain Model project uses MediatR. MediatR is simple mediator implementation in .NET which supports in-process messaging with no dependencies.
MediatR supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatching via C# generic variance.
In CQRS the fundamental idea is that we should divide an object's methods into two sharply separated categories:
- Queries: Return a result and do not change the observable state of the system (are free of side effects).
- Commands: Change the state of a system but do not return a value.
In the Domain Model-project we use these concepts from MediatR:
- Response: the output of a CommandHandler/QueryHandler.
- Commands: contains the input parameters for the CommandHandlers in combination with the response object.
- CommandHandlers: A service that perform the changes to the domain model.
- Queries: contains the input parameters for the QueryHandlers in combination with the response object.
- QueryHandlers: A service that query the database and returns the result.
public record ProfileUpdateResponse
{
public Profile? Profile { get; set; }
}
public record ProfileUpdateCommand : ICommand<ProfileUpdateResponse>
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
}
public class ProfileUpdateCommandHandler : ICommandHandler<ProfileUpdateCommand, ProfileUpdateResponse>
{
private readonly DomainModelTransaction domainModelTransaction;
private readonly IMediator mediator;
public ProfileUpdateCommandHandler(
DomainModelTransaction domainModelTransaction,
IMediator mediator)
{
this.domainModelTransaction = domainModelTransaction;
this.mediator = mediator;
}
public async Task<ProfileUpdateResponse> Handle(
ExampleCreateCommand command,
CancellationToken cancellationToken)
{
// code of the service
}
}
public record LoadAllProfilesResponse
{
public LoadAllProfilesResponse(IList<Profile> profiles)
{
Profiles = profiles;
}
public IList<Profile>? Profiles { get; }
}
public record LoadAllProfilesQuery : IQuery<LoadAllProfilesResponse>
{
}
public class LoadAllProfilesQueryHandler : IQueryHandler<LoadAllProfilesQuery, LoadAllProfilesResponse>
{
private readonly DomainModelTransaction domainModelTransaction;
public LoadAllProfilesQueryHandler(DomainModelTransaction domainModelTransaction)
{
this.domainModelTransaction = domainModelTransaction ?? throw new ArgumentNullException(nameof(domainModelTransaction));
}
public async Task<LoadAllProfilesResponse> Handle(LoadAllProfilesQuery request, CancellationToken cancellationToken)
{
// code of the service
}
}
As part of the MediatR flow we have injected a validation service right before the CommandHandler is executed. The validation service uses FluentValidation.
public class UpdateProfileCommandValidator : AbstractValidator<UpdateProfileCommand>
{
public UpdateProfileCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name cannot be empty");
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("Email address cannot be empty");
RuleFor(x => x.Email)
.EmailAddress()
.WithMessage("Please specify a valid email address");
RuleFor(x => x.Description)
.NotEmpty()
.WithMessage("Description cannot be empty");
RuleFor(x => x.Phone)
.NotEmpty()
.WithMessage("Phone cannot be empty");
}
}
A part of Event-Driven Architecture is Event Notification and MediatR supports in-process notifications. In MuffiNet notifications are used to send notifications to the frontend after an update has taken place. The idea is that the notification handles can be used to communicate between two (or more) bounded contexts. If needed MuffiNet can be extended to use a out-of-process system for notifications like RabbitMQ.
public class UpdatedProfileNotification : INotification
{
public UpdatedProfileNotification(IProfileModel model)
{
Model = model;
}
public IProfileModel Model { get; }
}
This notification handler uses SignalR to update the frontend when the profile has been updated.
public class ProfileUpdatedNotificationHandler : INotificationHandler<ProfileUpdatedNotification>
{
private readonly IProfileHubContract profileHub;
public ProfileUpdatedNotificationHandler(IProfileHubContract profileHub)
{
this.profileHub = profileHub;
}
public async Task Handle(ProfileUpdatedNotification notification, CancellationToken cancellationToken)
{
await profileHub.ProfileUpdated(new ProfileUpdatedMessage(notification.Model));
}
}