- .Net 8
- C#
- EF Core
- Docker
1 . All methods must be implemented using async and await
2 . If there are no files in the respective folder, a .keep file must be placed inside
3 . The overall architecture of the project follows the structure below ( very very important ) :
- src
- Core
- Domic.Common
- ClassConsts
- ClassCustoms
- ClassDelegates
- ClassExtensions
- ClassExceptions
- ClassHelpers
- ClassWrappers
- Domic.Domain ( entities define in here )
- Commons
- Contracts
- Interfaces
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
- Domic.UseCase ( business logics define in here )
- Commons
- Caches
- Contracts
- Interfaces
- Abstracts
- DTOs
- Exceptions
- Extensions
- Infrastructure
- Domic.Infrastructure ( implementation of all domain & usecase contracts and all packages install in here )
- Extensions
- Implementations.Domain
- Repositories
- C
- Q
- Services
- Implementations.UseCase
- Services
- Domic.Persistence ( database context and configs )
- Configs
- C
- Q
- Contexts
- C
- Q
- Migrations
- C ( any migration of database for command side )
- Q ( any migration of database for query side )
- Presentation
- Domic.WebAPI ( api )
- DTOs
- EntryPoints
- GRPCs ( google RPC call )
- HTTPs ( REST api )
- HUbs
- Frameworks
- Extensions
- Filters
- Middlewares
- test
- E2ETests
- Presentation ( load test with NBomber package )
- IntegrationTests
- Infrastructure ( mocking with NSubstitute package )
- Presentation ( mocking with NSubstitute package )
- UnitTests
- Core ( mocking with NSubstitute package )
- Infrastructure ( mocking with NSubstitute package )
- Presentation ( mocking with NSubstitute package )
Example :
- src
- Core
- Domic.Common
- ClassConsts
- ClassCustom
- ClassDelegates
- ClassExtensions
- ClassExceptions
- ClassHelpers
- ClassWrappers
- Domic.Domain
- Commons
- Contracts
- Interfaces
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
- Category
- Contracts
- Interfaces
ICategoryCommandRepository.cs
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
- Domic.UseCase
- Commons
- Caches
- Contracts
- Interfaces
INotificationService.cs
- Abstracts
- DTOs
- Exceptions
- Extensions
- CategoryUseCase
- Caches
- Commands
- Contracts
- Interfaces
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
- Infrastructure
- Domic.Infrastructure
- Extensions
- Implementations.Domain
- Repositories
- C
CategoryCommandRepository.cs
- Q
- Services
- Implementations.UseCase
- Services
NotificationService.cs
- Domic.Persistence
- Configs
- C
CategoryConfig.cs
- Q
- Contexts
- C
- Q
- Migrations
- C
- Q
- Presentation
- Domic.WebAPI
- DTOs
- EntryPoints
- GRPCs
- HTTPs
- V1
CategoryController.cs
- HUbs
- Frameworks
- Extensions
- Filters
- Middlewares
- test
- E2ETests
- Presentation
- IntegrationTests
- Infrastructure
- Presentation
- UnitTests
- Core
- Infrastructure
- Presentation
4 . Be sure to apply the changes to the following path in the migration file when you make changes to the entities in the Domain layer
Example :
- src
- Core
- Infrastructure
- Domic.Infrastructure
- Extensions
- Implementations.Domain
- Repositories
- C
- Q
- Services
- Implementations.UseCase
- Services
- Domic.Persistence
- Configs
- C
- Q
- Contexts
- C
- Q
- Migrations
- C ( any migration of database for command side - execute ef migration command base on Guidance.txt file )
- Q ( any migration of database for query side - execute ef migration command base on Guidance.txt file )
Guidance.txt
- Presentation
- test
- E2ETests
- IntegrationTests
- UnitTests
5 . Be sure to implement different tests for each business you implement according to the architecture mentioned in the instructions above; that is, E2E, Integration, and UnitTest tests for different layers of the project
6 . Make sure that any methods you create in Repositories or various service contracts in the inner Core layers are implemented in the concretes of those contracts and in the Infrastructure layer
7 . Make sure that you are use NSubstitute package for mocking Core Layers in Integration and UnitTest
8 . Make sure to put all the settings that the program requires in the following path
Example :
- src
- Presentation
- Domic.WebAPI
- Configs
Config.json
9 . Make sure to include all settings such as database connection string, API Key, etc. in the following path
Example :
- src
- Presentation
- Domic.WebAPI
- Properties
launchSettings.json
{
"profiles": {
"Domic.WebAPI": {
"environmentVariables": {
"Elastic-Host": "http://localhost:9200" ( example)
}
}
}
}
Every entity created in the respective path must have the following folders :
- Contracts
- Interfaces
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
Example :
- Category
- Contracts
- Interfaces
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
The location for creating an entity is as follows :
Example :
- Category
- Contracts
- Interfaces
- Abstracts
- Entities
Category.cs
- Enumerations
- Events
- Exceptions
- ValueObjects
To implement an entity class, follow the examples below :
1 . If the desired entity is for the Query part, it must inherit from the EntityQuery<string> class
Example :
public class TicketQuery : EntityQuery<string>
{
}2 . If the desired entity is for the Command part, it must inherit from the Entity<string> class and be implemented as a Rich Domain Model
Example :
public class Ticket : Entity<string>
{
}When implementing an entity for the Command part, consider the following :
1 . The class must have two constructors as shown in the example below :
Example :
public class Ticket : Entity<string>
{
//Fields
/*---------------------------------------------------------------*/
//ValueObjects
/*---------------------------------------------------------------*/
//Relations ( Navigation Properties )
/*---------------------------------------------------------------*/
//For EF Core
private Ticket(){}
//must have C# comment here
public Ticket(
IGlobalUniqueIdGenerator globalUniqueIdGenerator, IDateTime dateTime,
IIdentityUser identityUser, ISerializer serializer
)
{
var roles = serializer.Serialize(identityUser.GetRoles());
var nowDateTime = DateTime.Now;
var nowPersianDate = dateTime.ToPersianShortDate(nowDateTime);
Id = globalUniqueIdGenerator.GetRandom(6);
//audit
CreatedBy = identityUser.GetIdentity();
CreatedRole = roles;
CreatedAt = new CreatedAt(nowDateTime, nowPersianDate);
}
}According to the above code, the following contracts must be injected into the public constructor :
- IGlobalUniqueIdGenerator
- IDateTime
- IIdentityUser
- ISerializer
2 . If a method is written in the desired entity that edits the entity, it must be implemented as shown in the example below :
Example :
public class Ticket : Entity<string>
{
//Fields
/*---------------------------------------------------------------*/
//ValueObjects
/*---------------------------------------------------------------*/
//Relations ( Navigation Properties )
/*---------------------------------------------------------------*/
//For EF Core
private Ticket(){}
//must have C# comment here
public Ticket(
IGlobalUniqueIdGenerator globalUniqueIdGenerator, IDateTime dateTime,
IIdentityUser identityUser, ISerializer serializer
)
{
var roles = serializer.Serialize(identityUser.GetRoles());
var nowDateTime = DateTime.Now;
var nowPersianDate = dateTime.ToPersianShortDate(nowDateTime);
Id = globalUniqueIdGenerator.GetRandom(6);
//audit
CreatedBy = identityUser.GetIdentity();
CreatedRole = roles;
CreatedAt = new CreatedAt(nowDateTime, nowPersianDate);
//raise event
AddEvent(
new TicketCreated {
Id = Id,
CreatedBy = CreatedBy,
CreatedRole = roles,
CreatedAt_EnglishDate = nowDateTime,
CreatedAt_PersianDate = nowPersianDate
}
);
}
/*---------------------------------------------------------------*/
//Behaviors
//must have C# comment here
public void Change(IDateTime dateTime, ISerializer serializer, IIdentityUser identityUser)
{
var roles = serializer.Serialize(identityUser.GetRoles());
var nowDateTime = DateTime.Now;
var nowPersianDate = dateTime.ToPersianShortDate(nowDateTime);
//audit
UpdatedBy = identityUser.GetIdentity();
UpdatedRole = roles;
UpdatedAt = new UpdatedAt(nowDateTime, nowPersianDate);
//raise event
AddEvent(
new TicketUpdated {
Id = Id,
UpdatedRole = roles,
UpdatedAt_EnglishDate = nowDateTime,
UpdatedAt_PersianDate = nowPersianDate
}
);
}
}To create event classes, follow the path below :
Example :
- Category
- Contracts
- Interfaces
- Abstracts
- Entities
- Enumerations
- Events
TicketCreated.cs
- Exceptions
- ValueObjects
To implement event classes related to an entity, follow the example below :
Example :
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "Ticket_Ticket_Exchange")]
public class TicketCreated : CreateDomainEvent<string>
{
public required string CategoryId { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required int Status { get; init; }
public required int Priority { get; init; }
}General rules for defining an event are stated below :
Example :
//ExchangeType : Exchange.FanOut | Exchange.Direct | Exchange.Headers | Exchange.Topic
//FanOut-Exchange
//create event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange")]
public class TicketCreated : CreateDomainEvent<string> //any type of identity key
{
//payload
}
//update event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange")]
public class TicketUpdated : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//delete event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange")]
public class TicketDeleted : DeleteDomainEvent<string> //any type of identity key
{
//payload
}
/*---------------------------------------------------------------*/
//ExchangeType : Exchange.FanOut | Exchange.Direct | Exchange.Headers | Exchange.Topic
//Direct-Exchange
//create event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route")]
public class TicketCreated : CreateDomainEvent<string> //any type of identity key
{
//payload
}
//update event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route")]
public class TicketUpdated : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//delete event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route")]
public class TicketDeleted : DeleteDomainEvent<string> //any type of identity key
{
//payload
}If the service mentioned above, in addition to producing these events ( Producer ), is also a consumer of these events ( Consumer ), it must be implemented as follows :
Example :
//FanOut-Exchange
//create event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange", Queue = "queue")]
public class TicketCreated : CreateDomainEvent<string> //any type of identity key
{
//payload
}
//update event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange", Queue = "queue")]
public class TicketUpdated : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//delete event
[EventConfig(ExchangeType = Exchange.FanOut, Exchange = "exchange", Queue = "queue")]
public class TicketDeleted : DeleteDomainEvent<string> //any type of identity key
{
//payload
}
/*---------------------------------------------------------------*/
//Direct-Exchange
//create event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route", Queue = "queue")]
public class TicketCreated : CreateDomainEvent<string> //any type of identity key
{
//payload
}
//update event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route", Queue = "queue")]
public class TicketUpdated : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//delete event
[EventConfig(ExchangeType = Exchange.Direct, Exchange = "exchange", Route = "route", Queue = "queue")]
public class TicketDeleted : DeleteDomainEvent<string> //any type of identity key
{
//payload
}To implement event classes related to an entity, follow the example below :
Example :
[EventConfig(Topic = "Ticket")]
public class TicketCreated : CreateDomainEvent<string>
{
public required string CategoryId { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required int Status { get; init; }
public required int Priority { get; init; }
}General rules for defining an event are stated below :
Example :
//create event
[EventConfig(Topic = "Topic")]
public class Created : CreateDomainEvent<string> //any type of identity key
{
//payload
}
//update event
[EventConfig(Topic = "Topic")]
public class Updated : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//delete event
[EventConfig(Topic = "Topic")]
public class Deleted : DeleteDomainEvent<string> //any type of identity key
{
//payload
}To create contracts that an entity needs, follow the example and specified path below :
Example :
- Category
- Contracts
- Interfaces
ICategoryCommandRepository.cs
- Abstracts
- Entities
- Enumerations
- Events
- Exceptions
- ValueObjects
To implement the repository interface for the respective entity, follow the examples below :
Example 1 :
public interface ICategoryCommandRepository : ICommandRepository<Category, string>
{
//custom contracts
}Example 2 :
public interface ICategoryQueryRepository : IQueryRepository<CategoryQuery, string>
{
//custom contracts
}For each entity in the Domain layer, a folder must be created with the following structure :
- Caches
- Commands
- Contracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
Example :
- CategoryUseCase
- Caches
- Commands
- Contracts
- Interfaces
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
The location for creating a Cache manager is as follows :
Example :
- CategoryUseCase
- Caches
AllCategoryInternalDistributedCache.cs
- Commands
- Contracts
- Interfaces
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
To implement a Cache manager class, follow the examples below :
Example :
//for current service distributed cache
public class AllCategoryInternalDistributedCache : IInternalDistributedCacheHandler<List<Dto>>
{
public AllCategoryInternalDistributedCache(){}
[Config(Key = 'Key', Ttl = 60 /*time to live based on minute*/)]
public List<Dto> Set()
{
//query
return new();
}
//must be used
[Config(Key = 'Key', Ttl = 60 /*time to live based on minute*/)]
public Task<List<Dto>> SetAsync(CancellationToken cancellationToken)
{
//query
return Task.FromResult(new());
}
}
/*---------------------------------------------------------------*/
//for all services distributed cache (global | shared cache)
public class AllCategoryExternalDistributedCache : IExternalDistributedCacheHandler<List<Dto>>
{
public AllCategoryExternalDistributedCache(){}
[Config(Key = 'Key', Ttl = 60 /*time to live based on minute*/)]
public List<Dto> Set()
{
//query
return new();
}
//must be used
[Config(Key = 'Key', Ttl = 60 /*time to live based on minute*/)]
public Task<List<Dto>> SetAsync(CancellationToken cancellationToken)
{
//query
return Task.FromResult(new());
}
}In the implementation of Cache according to the above instructions, there are a few points to note :
1 . If you do not set a value for Ttl in the ConfigAttribute above, or set this Property to 0, the corresponding Cache will remain permanently and without expiration in Redis
2 . To use the cached value ( according to the above instructions ), you must use the interface corresponding to InternalCache or ExternalCache. For this purpose, two interfaces IInternalDistributedCacheMediator and IExternalDistributedCacheMediator have been implemented, which can be used as follows :
Example :
public class Query : IQuery<List<Dto>>
{
}
public class QueryHandler : IQueryHandler<Query, List<Dto>>
{
private readonly IInternalDistributedCacheMediator _cacheMediator;
public QueryHandler(IInternalDistributedCacheMediator cacheMediator) => _cacheMediator = cacheMediator;
public List<Dto> Handle(Query query)
{
var result = _cacheMediator.Get<List<Dto>>(cancellationToken);
return result;
}
//must be used
public async Task<List<Dto>> HandleAsync(Query query, CancellationToken cancellationToken)
{
var result = await _cacheMediator.GetAsync<List<Dto>>(cancellationToken);
return result;
}
}The location for creating a Command manager is as follows :
Example :
- CategoryUseCase
- Caches
- Commands
- Create
CreateCommand.cs
CreateCommandHandler.cs
CreateCommandValidator.cs
- Contracts
- Interfaces
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
To implement a Command manager class, follow the examples below :
Example :
public class CreateCommand : ICommand<string> //any result type
{
//some properties
}
public class CreateCommandHandler : ICommandHandler<CreateCommand, string>
{
public CreateCommandHandler(){}
[WithTransaction]
public string Handle(CreateCommand command)
{
//logic
return default;
}
//must be used
[WithTransaction]
public Task<string> HandleAsync(CreateCommand command, CancellationToken cancellationToken)
{
//logic
return Task.FromResult<string>(default);
}
}In the implementation of the above codes, there are items that need to be managed if necessary :
1 . Using WithValidationAttribute
This Attribute is used when you need to validate your Command or Query. To start, you must create the corresponding Validator class and then apply WithValidation .
Example :
public class CreateCommandValidator : IValidator<CreateCommand>
{
public CreateCommandValidator(){}
public object Validate(CreateCommand input)
{
//validations
return default;
}
//must be used
public Task<object> ValidateAsync(CreateCommand input, CancellationToken cancellationToken)
{
//validations
return Task.FromResult(default(object));
}
}2 . In the above code, in the section related to the corresponding Validator class, you can use the result of the Validate or ValidateAsync method, which is an object, inside the corresponding CommandHandler
To do this, simply create a readonly variable of type object named _validationResult in your CommandHandler .
Example :
public class CreateCommand : ICommand<string> //any result type
{
//some properties
}
public class CreateCommandHandler : ICommandHandler<CreateCommand, string>
{
private readonly object _validationResult;
public CreateCommandHandler(){}
[WithValidation]
[WithTransaction]
public string Handle(CreateCommand command)
{
//logic
return default;
}
[WithValidation]
[WithTransaction]
public Task<string> HandleAsync(CreateCommand command, CancellationToken cancellationToken)
{
//logic
return Task.FromResult<string>(default);
}
}3 . Using WithCleanCacheAttribute
In the Command section, when you need to delete the Cache related to the desired entity after executing the logic of the relevant section, so that the corresponding Cache is created again in another request sent for the relevant Query section, you can use this Attribute according to the codes below .
Example :
public class CreateCommand : ICommand<string> //any result type
{
//some properties
}
public class CreateCommandHandler : ICommandHandler<CreateCommand, string>
{
public CreateCommandHandler(){}
[WithCleanCache(Keies = "Key1|Key2|...")]
public string Handle(CreateCommand command)
{
//logic
return default;
}
[WithCleanCache(Keies = "Key1|Key2|...")]
public Task<string> HandleAsync(CreateCommand command, CancellationToken cancellationToken)
{
//logic
return Task.FromResult<string>(default);
}
}4 . Using WithPessimisticConcurrencyAttribute
When you need to place the logic of your Command section, which is a Critical Section, inside a lock block so that only one or a specific number of Threads can access that Critical section, you can use this Attribute. For the Handle method, you must create a variable of type object in your CommandHandler, and for HandleAsync, you must create a variable of type SemaphoreSlim .
Example :
//for sync method (handle)
public class CreateCommand : ICommand<string> //any result type
{
//some properties
}
public class CreateCommandHandler : ICommandHandler<CreateCommand, string>
{
private static object _lock = new();
public CreateCommandHandler(){}
[WithPessimisticConcurrency]
public string Handle(CreateCommand command)
{
//logic
return default;
}
}
/*---------------------------------------------------------------*/
//for async method (handle async)
public class CreateCommand : ICommand<string> //any result type
{
//some properties
}
public class CreateCommandHandler : ICommandHandler<CreateCommand, string>
{
private static SemaphoreSlim _asyncLock = new(1, 1); //custom count of thread
public CreateCommandHandler(){}
[WithPessimisticConcurrency]
public Task<string> HandleAsync(CreateCommand command, CancellationToken cancellationToken)
{
//logic
return Task.FromResult<string>(default);
}
}Any contract other than repository-related contracts created in the Domain layer should be created in this folder .
Example :
- CategoryUseCase
- Caches
- Commands
- Create
CreateCommand.cs
CreateCommandHandler.cs
CreateCommandValidator.cs
- Contracts
- Interfaces
INotificationService.cs
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
To implement an Event manager class, follow the examples below .
Example :
//define in [Domain] layer of consumer service
[EventConfig(Queue = "queue")]
public class UpdatedEvent : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//define in [UseCase] layer of consumer service
public class UpdatedConsumerEventBusHandler : IConsumerEventBusHandler<UpdatedEvent>
{
public UpdatedConsumerEventBusHandler(){}
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
//must be used
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}In the implementation of the above instructions, a few points are necessary to mention :
1 . Using WithMaxRetryAttribute
This Attribute allows you to manage the retry attempts of the corresponding Consumer for processing the relevant Message or Event .
To use this Attribute, you can follow the instructions below .
Example :
//for [Event] consuming
public class UpdatedConsumerEventBusHandler : IConsumerEventBusHandler<UpdatedEvent>
{
public UpdatedConsumerEventBusHandler(){}
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}In using WithMaxRetryAttribute, there is a feature called HasAfterMaxRetryHandle, which indicates whether there is a need to separately manage the relevant message if it has been retried more than the allowed limit. If this feature is set to false, which is the default value of this variable, the relevant message will be removed from the corresponding Queue after the maximum attempt to process it .
If the desired message in the corresponding Queue reaches the maximum retry limit (in case of possible errors), to separately manage the processing of the relevant message, you must follow the instructions below .
Example :
//for [Event] consuming
public class UpdatedConsumerEventBusHandler : IConsumerEventBusHandler<UpdatedEvent>
{
public UpdatedConsumerEventBusHandler(){}
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
//must be used
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
//for handle max retry
public void AfterMaxRetryHandle(UpdatedEvent @event)
{
//logic
}
//must be used
public Task AfterMaxRetryHandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}2 . Using WithCleanCacheAttribute
As explained in the Mediator tool, here too, we can use this Attribute according to the previously mentioned instructions .
To implement an Event manager class, follow the examples below .
Example :
//define in [Domain] layer of consumer service
[EventConfig(Topic = "Topic")]
public class UpdatedEvent : UpdateDomainEvent<string> //any type of identity key
{
//payload
}
//define in [UseCase] layer of consumer service
public class UpdatedConsumerEventStreamHandler : IConsumerEventStreamHandler<UpdatedEvent>
{
public UpdatedConsumerEventStreamHandler(){}
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
//must be used
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}In the implementation of the above instructions, a few points are necessary to mention :
1 . Using WithMaxRetryAttribute
This Attribute allows you to manage the retry attempts of the corresponding Consumer for processing the relevant Message or Event .
To use this Attribute, you can follow the instructions below .
Example :
//for [Event] consuming
public class UpdatedConsumerEventStreamHandler : IConsumerEventStreamHandler<UpdatedEvent>
{
public UpdatedConsumerEventStreamHandler(){}
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
//must be used
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}In using WithMaxRetryAttribute, there is a feature called HasAfterMaxRetryHandle, which indicates whether there is a need to separately manage the relevant message if it has been retried more than the allowed limit. If this feature is set to false, which is the default value of this variable, the relevant message will be removed from the corresponding Queue after the maximum attempt to process it .
If the desired message in the corresponding Queue reaches the maximum retry limit (in case of possible errors), to separately manage the processing of the relevant message, you must follow the instructions below .
Example :
//for [Event] consuming
public class UpdatedConsumerEventStreamHandler : IConsumerEventStreamHandler<UpdatedEvent>
{
public UpdatedConsumerEventStreamHandler(){}
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public void Handle(UpdatedEvent @event)
{
//logic
}
//must be used
[WithMaxRetry(Count = 100, HasAfterMaxRetryHandle = true)] //Count = 100 -> this message will be reprocessed a maximum of 100 times in case of an error
[TransactionConfig(Type = TransactionType.Command)] //or => Type = TransactionType.Query
public Task HandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
//for handle max retry
public void AfterMaxRetryHandle(UpdatedEvent @event)
{
//logic
}
//must be used
public Task AfterMaxRetryHandleAsync(UpdatedEvent @event, CancellationToken cancellationToken)
{
//logic
return Task.CompleteTask;
}
}2 . Using WithCleanCacheAttribute
As explained in the Mediator tool, here too, we can use this Attribute according to the previously mentioned instructions .
The location for creating a Query manager is as follows :
Example :
- CategoryUseCase
- Caches
- Commands
- Contracts
- Interfaces
- Abstracts
- DTOs
- Events
- Exceptions
- Extensions
- Queries
- ReadAllPagination
ReadAllPaginationQuery.cs
ReadAllPaginationQueryHandler.cs
ReadAllPaginationQueryValidator.cs
To implement a Query manager class, follow the examples below .
Example :
public class ReadAllQuery : IQuery<Dto> //any result type
{
}
public class ReadAllQueryHandler : IQueryHandler<ReadAllQuery, Dto>
{
public ReadAllQueryHandler(){}
public Dto Handle(ReadAllQuery query)
{
//query
return default;
}
//must be used
public Task<Dto> HandleAsync(ReadAllQuery query, CancellationToken cancellationToken)
{
//query
return Task.FromResult<Dto>(default);
}
}The structure of this layer is as follows :
- Configs
- C
- Q
- Contexts
- C
- Q
- Migrations
- C
- Q
Example :
- Configs
- C
CategotyConfig.cs
- Q
CategoryQueryConfig.cs
- Contexts
- C
SQLContext.cs
SQLContextFactory.cs
- Q
SQLContext.cs
SQLContextFactory.cs
- Migrations
- C
- Q
Regarding the above structure, there are some points to note :
1 . All entities created in the Domain layer must be defined in the SQLContext.cs file
Example :
/*Setting*/
public partial class SQLContext : DbContext
{
public SQLContext(DbContextOptions<SQLContext> options) : base(options)
{
}
}
/*Entity*/
public partial class SQLContext
{
public DbSet<ConsumerEventQuery> ConsumerEvents { get; set; }
public DbSet<TicketQuery> Tickets { get; set; }
public DbSet<TicketCommentQuery> TicketComments { get; set; }
public DbSet<CategoryQuery> Categories { get; set; }
public DbSet<UserQuery> Users { get; set; }
}
/*Config*/
public partial class SQLContext
{
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfiguration(new ConsumerEventQueryConfig());
builder.ApplyConfiguration(new TicketQueryConfig());
builder.ApplyConfiguration(new CategoryQueryConfig());
builder.ApplyConfiguration(new UserQueryConfig());
}
}2 . To create a Migration file, you must follow the instructions in the Guidance.txt file
3 . All entity configurations of the Domain layer must be placed in the Configs folder
Example :
//for [Query] entity
public class CategoryQueryConfig : BaseEntityQueryConfig<CategoryQuery, string>
{
public override void Configure(EntityTypeBuilder<CategoryQuery> builder)
{
base.Configure(builder);
//Configs
builder.ToTable("Categories");
//relations
builder.HasMany(category => category.Tickets)
.WithOne(ticket => ticket.Category)
.HasForeignKey(ticket => ticket.CategoryId);
}
}
/*---------------------------------------------------------------*/
//for [Command] entity
public class CategoryConfig : BaseEntityConfig<Category, string>
{
public override void Configure(EntityTypeBuilder<Category> builder)
{
base.Configure(builder);
//Configs
builder.ToTable("Categories");
//relations
builder.HasMany(category => category.Tickets)
.WithOne(ticket => ticket.Category)
.HasForeignKey(ticket => ticket.CategoryId);
}
}The infrastructure layer, where all contract implementations built in the Domain and UseCase layers must be implemented, according to the following pattern :
- Extensions
- Implementations.Domain
- Repositories
- C
- Q
- Services
- Implementations.UseCase
- Services
Example :
- Extensions
- Implementations.Domain
- Repositories
- C
UserCommandRepository.cs ( UserCommandRepository inherit from IUserCommandRepository interface in domain layer )
- Q
UserQueryRepository.cs ( UserQueryRepository inherit from IUserQueryRepository interface in domain layer )
- Services
UserValidationService.cs ( UserValidationService inherit from IUserValidationService interface in domain layer )
- Implementations.UseCase
- Services
NotificationService.cs ( NotificationService inherit from INotificationService interface in usecase layer )
This layer is the last layer of the project and contains appropriate EndPoints with gRPC, HTTP, and Hub inside it, which follows the following structure. :
- Configs
- CoreLogs
- EntryPoints
- GRPCs
- HTTPs
- Hubs
- Frameworks
- Middlewares
- Filters
- Extensions
Example :
- Configs
- CoreLogs
- EntryPoints
- GRPCs
UserRpcController.cs
- HTTPs
- V1
UserController.cs
- Hubs
- Frameworks
- Middlewares
- Filters
- Extensions