In this blog we will learn how to implement CQRS, MediaTR, Event Sourcing and Message Broker Patterns in Asp.Net Core Web API project.
Topics To know Before: | Clean Architecture, Entity Framework (Code First Approach) |
Development Tools: | VS 2022, SQL Server Database (Latest), RabbitMQ |
.Net Version: | 8.0 |
Nuget Packages (Mediator, Broker) | MediaTR RabbitMQ.Client |
Part One: Clean Architecture Asp.Net Core Web API using CQRS, MediaTR, Event Sourcing and Message Broker Patterns
This is a simple crud app with Clean Architecture using Entity Framework Code First Approach. There is an entity class named Post and in this app I just implemented crud on it. The app solution contains seven Library projects and one API project.
CleanArchitecture.Domain
- Contains entities, models. view models.
CleanArchitecture.Application.Common
- Contains all Interfaces which will be implemented in the infrastructure layer.
- Added CleanArchitecture.Domain project as reference.
CleanArchitecture.Application.Commands
- Contains all Commands (Insert, Edit, Delete).
- Commands will not save the data changes into the database.
- Commands publish data into brokers (RabbitMQ) then return the response immediately.
- The Broker passes the data to the Broker manager service.
- Broker manager service executes events based on command type (Insert, Edit, Delete).
- The Event Handler of the specific event will save the data changes to the database.
- Then the event handler sends notification about the operation completion to the User.
- Added CleanArchitecture.Application.Common project as reference.
- Installed the MediaTR NuGet package on it.
- Installed the RabbitMQ.Client NuGet package on it.
CleanArchitecture.Application.Queries
- Contains all Queries (Select data only).
- Added CleanArchitecture.Application.Common project as reference.
- Installed the MediaTR NuGet package on it.
CleanArchitecture.Infrastructure.Common
- Contains DBContext class.
- Contains Migration folder.
CleanArchitecture.Infrastructure.Commands
- Contains repositories to add, update, delete data in the database using DBContext.
- Added CleanArchitecture.Application.Common project as reference.
- Implements the interfaces of CleanArchitecture.Application.Common project.
CleanArchitecture.Infrastructure.Queries
Contains repositories to Query data in the database using DBContext.
- Added CleanArchitecture.Application.Common project as reference.
WebAPI
- Contains API Controllers which use the services of Application project.
- Added all Command and Query projects of Application and Infrastructure layer as reference.
- Installed the MediaTR NuGet package on it.
CQRS
It’s a project architectural pattern. It separates all query and command codes into different sources. In this blog we used different library projects to separate commands and queries. The full meaning of CQRS is Command Query Responsibility Segregation.
MediaTR
It’s a library of .Net. It implements the Mediator design pattern. So, we can use it to implement Mediator in our project. The Mediator design patterns uses loose coupling and provide a flexible way to communicate between one more class without using their concrete implementation.
RabbitMQ
It’s a message broker. The message broker is a tool which will help you to send your command to the appropriate event handler. Then the event handler will process the command and create notification for users to inform them that their request is processed. It’s necessary to implement broker pattern.
A broker saves your requests from being lost and keeps them secure until being processed by the event handler. When a large amount of requests come from the users or application server unavailable then it saves the requests on it and push them to event handlers when server available Again.
Also we can release users after submit a request to the broker before processing it and user will be notified by event handlers after processing the request. It will provide a good user experience cause waiting for a long time is really bad from a user’s perspective.
There are one more message brokers available on the web. e.g: Azure Service Bus, Kafka e.t.c. But, in this blog we use only RabbitMQ.
Event Sourcing
Event Sourcing is a software architectural pattern. It emphasizes storing event data into an event store database. It helps us to find out when the event comes from user, what data was passed with the request,
Is it processed or not and when it’s processed. Each event stored in the Event Store has an aggregator id which is used to track the event later if necessary. Not only the event tracking it also provides a large area to make new useful features and business level decisions.
CleanArchitecture.Domain Project: It Contains all models, entities and view models.
The Entity Post.cs is as like as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CleanArchitecture.Domain.Entities
{
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
}
We use an entity to store our events for event sourcing which is Event. The event entity is as like as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CleanArchitecture.Domain.EventStore.Entities
{
public class Event
{
public int Id { get; set; }
public string AggregateId { get; set; }
public string DataJson { get; set; }
public string DataType { get; set; }
public DateTime EventTime { get; set; }
public bool IsCompleted { get; set; }
}
}
Part Two : CQRS, MediaTR, Event Sourcing and Message Broker Patterns
This is the part two of Event Based Clean Architecture Implementation. We have already covered the first part.
CleanArchitecture.Application.Common: This project contains all interfaces which will be implemented in the Infrastructure layer.
Here the Queries and Commands interfaces are separated by commands and queries folder. This is a concept of CQRS. Also I separated them by different files.
Commands
IEventsRepository: It’s for event sourcing related works.
using CleanArchitecture.Domain.EventStore.Entities;
namespace CleanArchitecture.Application.Common.IRepositories.Commands
{
public interface IEventsRepository
{
Task StoreEvent(Event eventData);
Task CompleteEvent(string aggregateId);
}
}
IPostsRepository: It’s for all query type operations on entity Post
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.Common.IRepositories.Commands
{
public interface IPostsRepository
{
Task InsertPost(Post objPost);
Task UpdatePost(Post objPost);
bool DeletePost(int ID);
}
}
Queries
IPostsRepository: It’s for all query type operations on entity Post.
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.Common.IRepositories.Queries
{
public interface IPostsRepository
{
Task> GetPosts();
Task GetPostByID(int ID);
}
}
CleanArchitecture.Application.Commands: All Commands related business logic are implemented here. Also event sourcing, publishing to broker and event handling done inside this project.
Broker Manager
This portion holds the codes for publishing event to broker and consume them means
release them to appropriate event handlers.
IBrokerManager:
namespace CleanArchitecture.Application.Commands.BrokerManager
{
public interface IBrokerManager
{
public void publish(object data, string eventType);
public void consume();
}
}
RabbitMqManager:
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MediatR;
using Microsoft.Extensions.Configuration;
namespace CleanArchitecture.Application.Commands.BrokerManager
{
public class RabbitMqManager : IBrokerManager
{
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly IConfiguration _configuration;
public RabbitMqManager(ILogger logger,
IMediator mediator,
IConfiguration configuration)
{
_logger = logger;
_mediator = mediator;
_configuration = configuration;
}
public static Type GetType(string typeName)
{
var type = Type.GetType(typeName);
if (type != null) return type;
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
{
type = a.GetType(typeName);
if (type != null)
return type;
}
return null;
}
public void consume()
{
try
{
var factory = new ConnectionFactory {
HostName = _configuration.GetSection("AppSettings")["RabbitMQHost"],
Port = AmqpTcpEndpoint.UseDefaultPort,
UserName = _configuration.GetSection("AppSettings")["RabbitMQUsername"],
Password = _configuration.GetSection("AppSettings")["RabbitMQPassword"],
VirtualHost = "/",
ContinuationTimeout = new TimeSpan(10, 0, 0, 0)
};
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();
channel.QueueDeclare(queue: "PostCommandQueue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind("PostCommandQueue", "PostCommands", "PostEvents");
Console.WriteLine(" [*] Waiting for messages.");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
_logger.LogInformation("Publish Event: " + ea.BasicProperties.Type);
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var eventType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes()).Where(t =>
string.Equals(t.Name, ea.BasicProperties.Type, StringComparison.Ordinal)).First();
var eventData = JsonConvert.DeserializeObject(message, eventType);
_logger.LogInformation("Publish Event Data: " + JsonConvert.SerializeObject(eventData));
await _mediator.Publish(eventData);
};
channel.BasicConsume(queue: "PostCommandQueue",
autoAck: true,
consumer: consumer);
System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
public void publish(object data, string eventType)
{
try
{
_logger.LogInformation("Publish To Broker: " + JsonConvert.SerializeObject(data));
var factory = new ConnectionFactory
{
HostName = _configuration.GetSection("AppSettings")["RabbitMQHost"],
Port = AmqpTcpEndpoint.UseDefaultPort,
UserName = _configuration.GetSection("AppSettings")["RabbitMQUsername"],
Password = _configuration.GetSection("AppSettings")["RabbitMQPassword"],
VirtualHost = "/",
ContinuationTimeout = new TimeSpan(10, 0, 0, 0)
};
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();
channel.QueueDeclare(queue: "PostCommandQueue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data));
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
properties.ContentType = "application/json";
properties.Type = eventType;
channel.BasicPublish(exchange: "PostCommands",
routingKey: "PostEvents",
basicProperties: properties,
body: body);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
Create Post Command
It saves the event to the event store database and publishes them to the broker.
CreatePostCommand class:
using AutoMapper;
using CleanArchitecture.Application.Commands.BrokerManager;
using CleanArchitecture.Application.Commands.Events.PostEvents.CreatePostEvent;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Domain.EventStore.Entities;
using CleanArchitecture.Domain.ViewModels;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Commands.Services.PostServices.CreatePostCommand
{
public class CreatePostCommand : IRequest
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
public class CreatePostCommandHandler : IRequestHandler
{
private readonly IEventsRepository _eventRepository;
private readonly IBrokerManager _brokerManager;
private readonly ILogger _logger;
private readonly IMapper _mapper;
public CreatePostCommandHandler(IBrokerManager brokerManager,
IEventsRepository eventRepository,
ILogger logger,
IMapper mapper)
{
_brokerManager = brokerManager;
_eventRepository = eventRepository;
_logger = logger;
_mapper = mapper;
}
public async Task Handle(CreatePostCommand command, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Create Post Command: " + JsonConvert.SerializeObject(command));
var validationErrors = new Dictionary();
if (String.IsNullOrEmpty(command.Title))
{
validationErrors["Title"] = "Title can not be empty.";
return ResponseModel.validationErrors(validationErrors);
}
string id = await _eventRepository.StoreEvent(
new Event()
{
DataJson = JsonConvert.SerializeObject(command),
DataType = command.GetType().Name,
}
);
if (id != null)
{
CreatePostEvent eventData = _mapper.Map(command);
eventData.AggregateId = id;
_brokerManager.publish(eventData, eventData.GetType().Name);
return ResponseModel.ok(id);
}
else
{
return ResponseModel.customError("Unexpected Error");
}
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
AutoMapper:
using AutoMapper;
using CleanArchitecture.Application.Commands.Events.PostEvents.CreatePostEvent;
namespace CleanArchitecture.Application.Commands.Services.PostServices.CreatePostCommand
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap();
}
}
}
Edit Post Command
It saves the event to the event store and publishes it to the broker.
EditPostCommand class:
using AutoMapper;
using CleanArchitecture.Application.Commands.BrokerManager;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Application.Events.PostEvents.EditPostEvent;
using CleanArchitecture.Domain.EventStore.Entities;
using CleanArchitecture.Domain.ViewModels;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Commands.Services.PostServices.EditPostCommand
{
public class EditPostCommand : IRequest
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
public class EditPostCommandHandler : IRequestHandler
{
private readonly IBrokerManager _brokerManager;
private readonly IEventsRepository _eventRepository;
private readonly Common.IRepositories.Queries.IPostsRepository _postsQueryRepository;
private readonly ILogger _logger;
private readonly IMapper _mapper;
public EditPostCommandHandler(IBrokerManager brokerManager,
IEventsRepository eventRepository,
Common.IRepositories.Queries.IPostsRepository postsQueryRepository,
ILogger logger,
IMapper mapper)
{
_brokerManager = brokerManager;
_eventRepository = eventRepository;
_postsQueryRepository = postsQueryRepository;
_logger = logger;
_mapper = mapper;
}
public async Task Handle(EditPostCommand command, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Edit Post Command: " + JsonConvert.SerializeObject(command));
var validationErrors = new Dictionary();
if (String.IsNullOrEmpty(command.Title))
{
validationErrors["Title"] = "Title can not be empty.";
return ResponseModel.validationErrors(validationErrors);
}
var post = await _postsQueryRepository.GetPostByID(command.Id);
if (post == null)
{
return ResponseModel.customError("No Post Found!");
}
string id = await _eventRepository.StoreEvent(
new Event()
{
DataJson = JsonConvert.SerializeObject(command),
DataType = command.GetType().Name,
}
);
if (id != null)
{
EditPostEvent eventData = _mapper.Map(command);
eventData.AggregateId = id;
_brokerManager.publish(eventData, eventData.GetType().Name);
return ResponseModel.ok(id);
}
else
{
return ResponseModel.customError("Unexpected Error");
}
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
AutoMapper:
using AutoMapper;
using CleanArchitecture.Application.Events.PostEvents.EditPostEvent;
namespace CleanArchitecture.Application.Commands.Services.PostServices.EditPostCommand
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap();
}
}
}
Delete Post Command
It saves the event to the event store and publishes it into the broker.
DeletePostCommand class:
using AutoMapper;
using CleanArchitecture.Application.Commands.BrokerManager;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Application.Events.PostEvents.DeletePostEvent;
using CleanArchitecture.Domain.EventStore.Entities;
using CleanArchitecture.Domain.ViewModels;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Commands.Services.PostServices.DeletePostCommand
{
public class DeletePostCommand : IRequest
{
public int Id { get; set; }
}
public class DeletePostCommandHandler : IRequestHandler
{
private readonly IBrokerManager _brokerManager;
private readonly IEventsRepository _eventRepository;
private readonly Common.IRepositories.Queries.IPostsRepository _postsQueryRepository;
private readonly ILogger _logger;
private readonly IMapper _mapper;
public DeletePostCommandHandler(IBrokerManager brokerManager,
IEventsRepository eventRepository,
Common.IRepositories.Queries.IPostsRepository postsQueryRepository,
ILogger logger,
IMapper mapper)
{
_brokerManager = brokerManager;
_eventRepository = eventRepository;
_postsQueryRepository = postsQueryRepository;
_logger = logger;
_mapper = mapper;
}
public async Task Handle(DeletePostCommand command, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Delete Post Command: " + JsonConvert.SerializeObject(command));
int ID = command.Id;
var post = await _postsQueryRepository.GetPostByID(ID);
if (post == null)
{
return ResponseModel.customError("No Post Found!");
}
string id = await _eventRepository.StoreEvent(
new Event()
{
DataJson = JsonConvert.SerializeObject(command),
DataType = command.GetType().Name,
}
);
if (id != null)
{
DeletePostEvent eventData = new DeletePostEvent() { Id = command.Id };
eventData.AggregateId = id;
_brokerManager.publish(eventData, eventData.GetType().Name);
return ResponseModel.ok(id);
}
else
{
return ResponseModel.customError("Unexpected Error");
}
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
Create Post Event
It consumes the create post event from the broker and then saves the new Post to the database.
CreatePostEvent class:
using AutoMapper;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Commands.Events.PostEvents.CreatePostEvent
{
public class CreatePostEvent : INotification
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AggregateId { get; set; }
}
public class CreatePostEventHandler : INotificationHandler
{
private readonly IEventsRepository _eventRepository;
private readonly IPostsRepository _postsRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public CreatePostEventHandler(IEventsRepository eventRepository,
IPostsRepository postsRepository,
IMapper mapper,
ILogger logger)
{
_eventRepository = eventRepository;
_postsRepository = postsRepository;
_mapper = mapper;
_logger = logger;
}
public async Task Handle(CreatePostEvent createPostEvent, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Create Post Event: " + JsonConvert.SerializeObject(createPostEvent));
Post post = _mapper.Map(createPostEvent);
int id = await _postsRepository.InsertPost(post);
if (id != 0)
{
///Send Confirmation Mail To User
}
else
{
///Send Error Information Mail To User
}
await _eventRepository.CompleteEvent(createPostEvent.AggregateId);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
AutoMapper:
using AutoMapper;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.Commands.Events.PostEvents.CreatePostEvent
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap();
}
}
}
Edit Post Event
It consumes the edit post event from the broker and saves the new changes on the Post to the database.
EditPostEvent class:
using AutoMapper;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Events.PostEvents.EditPostEvent
{
public class EditPostEvent : INotification
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AggregateId { get; set; }
}
public class EditPostEventHandler : INotificationHandler
{
private readonly IEventsRepository _eventRepository;
private readonly IPostsRepository _postsRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public EditPostEventHandler(IEventsRepository eventRepository,
IPostsRepository postsRepository,
IMapper mapper,
ILogger logger)
{
_eventRepository = eventRepository;
_postsRepository = postsRepository;
_mapper = mapper;
_logger = logger;
}
public async Task Handle(EditPostEvent editPostEvent, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Edit Post Event: " + JsonConvert.SerializeObject(editPostEvent));
Post objPost = _mapper.Map(editPostEvent);
bool result = await _postsRepository.UpdatePost(objPost);
if (!result)
{
// send error mail to user
}
else {
/// send success mail to user
}
await _eventRepository.CompleteEvent(editPostEvent.AggregateId);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
AutoMapper:
using AutoMapper;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.Events.PostEvents.EditPostEvent
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap();
}
}
}
Delete Post Event
It consumes the delete post event from the broker and deletes the Post from the database.
DeletePostEvent class:
using AutoMapper;
using CleanArchitecture.Application.Common.IRepositories.Commands;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Events.PostEvents.DeletePostEvent
{
public class DeletePostEvent : INotification
{
public int Id { get; set; }
public string AggregateId { get; set; }
}
public class DeletePostEventHandler : INotificationHandler
{
private readonly IEventsRepository _eventRepository;
private readonly IPostsRepository _postsRepository;
private readonly ILogger _logger;
public DeletePostEventHandler(IEventsRepository eventRepository,
IPostsRepository postsRepository,
ILogger logger)
{
_eventRepository = eventRepository;
_postsRepository = postsRepository;
_logger = logger;
}
public async Task Handle(DeletePostEvent deletePostEvent, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Delete Post Event: " + JsonConvert.SerializeObject(deletePostEvent));
int ID = deletePostEvent.Id;
bool response = _postsRepository.DeletePost(ID);
if (!response)
{
// send error mail to user
}
else
{
//send success mail to user
}
await _eventRepository.CompleteEvent(deletePostEvent.AggregateId);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
DependencyInjections: It holds all dependencies that need to register in a web api project.
using CleanArchitecture.Application.Commands.BrokerManager;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace CleanArchitecture.Application.Commands
{
public static class DependencyInjections
{
public static IServiceCollection AddApplicationCommandServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
//for mediatr only
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
});
//
services.AddScoped();
return services;
}
}
}
DependencyInjections: It holds all dependencies that need to register in a web api project.
GetPostByIdQuery:
using AutoMapper;
using CleanArchitecture.Application.Common.IRepositories.Queries;
using CleanArchitecture.Domain.ViewModels;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Queries.Services.PostServices.GetPostByIdQuery
{
public class GetPostByIdQuery : IRequest
{
public int Id { get; set; }
}
public class GetPostByIdQueryHandler : IRequestHandler
{
private readonly IPostsRepository _postsQueryRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public GetPostByIdQueryHandler(IPostsRepository postsQueryRepository,
IMapper mapper,
ILogger logger)
{
_postsQueryRepository = postsQueryRepository;
_mapper = mapper;
_logger = logger;
}
public async Task Handle(GetPostByIdQuery query, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Get Post By Id Query: " + JsonConvert.SerializeObject(query));
var post = await _postsQueryRepository.GetPostByID(query.Id);
if (post == null)
{
return ResponseModel.customError("No Post Found!");
}
return ResponseModel.ok(post);
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
GetPostsQuery:
using AutoMapper;
using CleanArchitecture.Application.Common.IRepositories.Queries;
using CleanArchitecture.Domain.ViewModels;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CleanArchitecture.Application.Queries.Services.PostServices.GetPostsQuery
{
public class GetPostsQuery : IRequest
{
}
public class GetPostsQueryHandler : IRequestHandler
{
private readonly IPostsRepository _postsQueryRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public GetPostsQueryHandler(IPostsRepository postsQueryRepository,
IMapper mapper,
ILogger logger)
{
_postsQueryRepository = postsQueryRepository;
_mapper = mapper;
_logger = logger;
}
public async Task Handle(GetPostsQuery query, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Get Posts Query: " + JsonConvert.SerializeObject(query));
return ResponseModel.ok(await _postsQueryRepository.GetPosts());
}
catch (Exception ex)
{
_logger.LogError(message: ex.Message, ex);
throw;
}
}
}
}
DependencyInjections: This file holds all dependencies that need to register in the web api project.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace CleanArchitecture.Application.Queries
{
public static class DependencyInjections
{
public static IServiceCollection AddApplicationQueryServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
//for mediatr only
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
});
//
return services;
}
}
}
Part Three : Infrastructure And Project API
CleanArchitecture.Infrastructure.Common: This project holds two database contexts. One for entity Post and the other one is only for Event storing. Also the migration folder is inside it.
EventContext:
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.EventStore.Entities;
namespace CleanArchitecture.Infrastructure.Common.Data.EventStore
{
public class EventContext : DbContext
{
public EventContext() { }
public EventContext(DbContextOptions options) : base(options)
{
}
public DbSet Events { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
}
PostContext:
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Infrastructure.Common.Data
{
public class PostContext : DbContext
{
public PostContext() { }
public PostContext(DbContextOptions options) : base(options)
{
}
public DbSet Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
}
CleanArchitecture.Infrastructure.Commands: It contains all database related logics for commands and event storing.
EventsRepository: it implements IEventsRepository interface located in application layer in CleanArchitecture.Application.Common project.
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Domain.EventStore.Entities;
using CleanArchitecture.Infrastructure.Common.Data.EventStore;
using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.Infrastructure.Commands.Repositories
{
public class EventsRepository : IEventsRepository
{
private readonly EventContext _eventDBContext;
public EventsRepository(EventContext context)
{
_eventDBContext = context ??
throw new ArgumentNullException(nameof(context));
}
public async Task CompleteEvent(string aggregateId)
{
try
{
Event eventData = _eventDBContext.Events.FirstOrDefault(r => r.AggregateId == aggregateId);
eventData.IsCompleted = true;
_eventDBContext.Entry(eventData).State = EntityState.Modified;
await _eventDBContext.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
return false;
}
}
public async Task StoreEvent(Event eventData)
{
try
{
eventData.EventTime = DateTime.Now;
eventData.AggregateId = Guid.NewGuid().ToString();
_eventDBContext.Events.Add(eventData);
await _eventDBContext.SaveChangesAsync();
return eventData.AggregateId;
}
catch (Exception ex)
{
return null;
}
}
}
}
PostsRepository: it implements IPostsRepository interface located in the application layer in CleanArchitecture.Application.Common project.
DependencyInjections: This file contains all dependency injections that need to be registered in the web api project. There are dependencies registered for database contexts.
using CleanArchitecture.Application.Common.IRepositories.Commands;
using CleanArchitecture.Infrastructure.Commands.Repositories;
using CleanArchitecture.Infrastructure.Common.Data;
using CleanArchitecture.Infrastructure.Common.Data.EventStore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Commands
{
public static class DependencyInjections
{
public static IServiceCollection AddInfrastructureCommandServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
var connectionStringEventDB = configuration.GetConnectionString("EventDBConnection");
services.AddDbContext(options =>
options.UseSqlServer(connectionString));
services.AddDbContext(options =>
options.UseSqlServer(connectionStringEventDB));
services.AddScoped();
services.AddScoped();
return services;
}
}
}
CleanArchitecture.Infrastructure.Queries: This project contains all queries or data fetching operations in the database.
PostsRepository: It implements IPostsRepository interface located in the application layer in CleanArchitecture.Application.Common project.
using CleanArchitecture.Application.Common.IRepositories.Queries;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Infrastructure.Common.Data;
using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.Infrastructure.Queries.Repositories
{
public class PostsRepository : IPostsRepository
{
private readonly PostContext _postDBContext;
public PostsRepository(PostContext context)
{
_postDBContext = context ??
throw new ArgumentNullException(nameof(context));
}
public async Task> GetPosts()
{
return await _postDBContext.Posts.ToListAsync();
}
public async Task GetPostByID(int ID)
{
return await _postDBContext.Posts.FindAsync(ID);
}
}
}
DependencyInjections: As like other projects it contains a dependency injections class.
using CleanArchitecture.Application.Common.IRepositories.Queries;
using CleanArchitecture.Infrastructure.Common.Data;
using CleanArchitecture.Infrastructure.Queries.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Queries
{
public static class DependencyInjections
{
public static IServiceCollection AddInfrastructureQueryServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
services.AddDbContext(options =>
options.UseSqlServer(connectionString));
services.AddScoped();
return services;
}
}
}
CleanArchitecture.WebAPI: This project contains all APIs of the project.
PostsController:
using CleanArchitecture.Application.Commands.Services.PostServices.CreatePostCommand;
using CleanArchitecture.Application.Commands.Services.PostServices.DeletePostCommand;
using CleanArchitecture.Application.Commands.Services.PostServices.EditPostCommand;
using CleanArchitecture.Application.Queries.Services.PostServices.GetPostByIdQuery;
using CleanArchitecture.Application.Queries.Services.PostServices.GetPostsQuery;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CleanArchitecture.WebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PostController : ControllerBase
{
private readonly IMediator _mediator;
public PostController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
[Route("GetPost")]
public async Task GetPosts()
{
return Ok(await _mediator.Send(new GetPostsQuery()));
}
[HttpGet]
[Route("GetPostByID/{Id}")]
public async Task GetPostByID(int Id)
{
return Ok(await _mediator.Send(new GetPostByIdQuery() { Id = Id }));
}
[HttpPost]
[Route("AddPost")]
public async Task AddPost(CreatePostCommand command)
{
return Ok(await _mediator.Send(command));
}
[HttpPut]
[Route("UpdatePost")]
public async Task UpdatePost(EditPostCommand command)
{
return Ok(await _mediator.Send(command));
}
[HttpDelete]
[Route("DeletePost")]
public async Task DeletePost(int id)
{
return Ok(await _mediator.Send(new DeletePostCommand() { Id = id}));
}
}
}
Program: This file contains all dependency registration and middlewares.
using CleanArchitecture.Application.Queries;
using CleanArchitecture.Application.Commands;
using CleanArchitecture.Infrastructure.Queries;
using CleanArchitecture.Infrastructure.Commands;
using CleanArchitecture.Application.Commands.BrokerManager;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationCommandServices(builder.Configuration);
builder.Services.AddApplicationQueryServices(builder.Configuration);
builder.Services.AddInfrastructureCommandServices(builder.Configuration);
builder.Services.AddInfrastructureQueryServices(builder.Configuration);
var provider = builder.Services.BuildServiceProvider();
var brokerManager = provider.GetRequiredService();
Thread brokerManagerThread = new Thread(new ThreadStart(brokerManager.consume));
brokerManagerThread.Start();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Note: Don’t forget to add below settings to your appsettings file.
"AppSettings": {
"RabbitMQHost": "RabbitMQ host Id",
"RabbitMQUsername": "your username",
"RabbitMQPassword": "your pass"
},
"ConnectionStrings": {
"DefaultConnection": "Application Database connection string",
"EventDBConnection": "Event Database connection string;"
}
RabbitMq installation guide: follow this.
If you face any challenges implementing ASP.NET Core Web API, we’re here to help. Hire Dedicated .NET Developers for Your Project from Vivasoft.
Our experts will provide solutions that meet your needs, ensuring your project is done with high quality and efficiency.
Contact us today to discuss your needs and let us deliver the best solution for your project.