Skip to content

Backend API

KlausEnevoldsen-Abtion edited this page Sep 26, 2023 · 2 revisions

Two starting points

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.

Domain Model

The principles behind the Domain Model is taken from Domain-Driven Design (DDD) and partly from Event-driven Architecture (EDA).

MediatR

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.

CQRS for separation of commands and queries

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.

Example of Response (Command)

public record ProfileUpdateResponse
{
    public Profile? Profile { get; set; }
}

Example of Command

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;
}

Example of CommandHandler

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
    }
}

Example of Response (Query)

public record LoadAllProfilesResponse
{
    public LoadAllProfilesResponse(IList<Profile> profiles)
    {
        Profiles = profiles;
    }

    public IList<Profile>? Profiles { get; }

}

Example of Query

public record LoadAllProfilesQuery : IQuery<LoadAllProfilesResponse>
{
}

Example of QueryHandler

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
    }
}

Validation with MediatR

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");
    }
}

In-process notifications

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.

Example of Notification (message)

public class UpdatedProfileNotification : INotification
{
    public UpdatedProfileNotification(IProfileModel model)
    {
        Model = model;
    }

    public IProfileModel Model { get; }
}

Example of NotificationHandler

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));
    }
}