Skip to content

Commit dde0d94

Browse files
committed
Atualiza arquitetura de comandos e gerenciamento de versões
* Melhoria na gestão de versões - Atualizados os valores de `DomainPreview`, `PersistPreview` e `PgVer`. - Referência ao `Npgsql.EntityFrameworkCore.PostgreSQL` agora usa `PgVer`. * Simplificação de referências de pacotes - Condição para `RoyalCode.SmartValidations` removida em `RoyalCode.Events.Outbox.Abstractions.csproj`. * Expansão da interface de unidade de trabalho - Adicionada propriedade `Db` em `IUnitOfWork` para acesso ao contexto. - `IWorkContext` agora inclui `ICommandDispatcher` para melhor funcionalidade. * Introdução de novas interfaces de comando - Novas interfaces para gerenciamento de comandos foram criadas. - Implementação da classe `CommandsConfigurer` para registrar manipuladores de comandos. * Implementação de gerenciamento de requisições de comando - Classe `CommandRequestHandler` adicionada para execução assíncrona de comandos. - Testes unitários em `CommandRequestTests.cs` para validar o comportamento de comandos.
1 parent 445e694 commit dde0d94

17 files changed

Lines changed: 615 additions & 7 deletions

File tree

RoyalCode.EnterprisePatterns/Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<DomainPreview>-preview-8.0</DomainPreview>
1111

1212
<PersistVer>1.0.0</PersistVer>
13-
<PersistPreview>-preview-8.5</PersistPreview>
13+
<PersistPreview>-preview-8.8</PersistPreview>
1414

1515
<CommandVer>0.1.0</CommandVer>
1616
<CommandPreview>-preview-1</CommandPreview>
@@ -21,6 +21,7 @@
2121
<PropertyGroup>
2222
<DotNetCoreVersion Condition="'$(TargetFramework)' == 'net8'">8.0.2</DotNetCoreVersion>
2323
<DotNetCoreVersion Condition="'$(TargetFramework)' == 'net9'">9.0.5</DotNetCoreVersion>
24+
<PgVer>9.0.4</PgVer>
2425
</PropertyGroup>
2526
<PropertyGroup>
2627
<ValVer>1.0.0-preview-1.0</ValVer>

RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Abstractions/RoyalCode.Events.Outbox.Abstractions.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
<PackageReference Include="Microsoft.Extensions.Options" Version="$(DotNetCoreVersion)" />
1919
</ItemGroup>
2020

21-
<ItemGroup Condition="'$(TargetFramework)' == 'net8'">
22-
<PackageReference Include="RoyalCode.SmartValidations" Version="$(ValVer)" />
21+
<ItemGroup>
22+
<PackageReference Include="RoyalCode.SmartValidations" Version="$(ValVer)" />
2323
</ItemGroup>
2424

2525
</Project>

RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.EntityFramework.Postgres/RoyalCode.Events.Outbox.EntityFramework.Postgres.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</PropertyGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(DotNetCoreVersion)" />
17+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(PgVer)" />
1818
</ItemGroup>
1919

2020
<ItemGroup>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
using Microsoft.Data.Sqlite;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using RoyalCode.Persistence.Tests.Entities;
5+
using RoyalCode.WorkContext.Abstractions;
6+
using RoyalCode.WorkContext.Abstractions.Commands;
7+
using RoyalCode.WorkContext.EntityFramework.Commands;
8+
using RoyalCode.SmartProblems;
9+
using Xunit;
10+
using RoyalCode.WorkContext.EntityFramework;
11+
using RoyalCode.UnitOfWork.Abstractions;
12+
13+
namespace RoyalCode.Persistence.Tests.WorkContext;
14+
15+
public class CommandRequestTests
16+
{
17+
[Fact]
18+
public async Task CommandRequestHandler_ThrowsIfNotConfigured()
19+
{
20+
// Arrange
21+
ServiceCollection services = new();
22+
services.AddWorkContext<CommandsDbContext>()
23+
.ConfigureDbContextPool((sp, builder) =>
24+
{
25+
SqliteConnection conn = new("DataSource=:memory:");
26+
conn.Open();
27+
builder.UseSqlite(conn);
28+
})
29+
.ConfigureRepositories(c => c.Add<Person>())
30+
.ConfigureCommands(c => { /* Não registra handler */ });
31+
32+
var root = services.BuildServiceProvider();
33+
var scope = root.CreateScope();
34+
var sp = scope.ServiceProvider;
35+
var db = sp.GetService<CommandsDbContext>();
36+
var context = sp.GetService<IWorkContext>();
37+
38+
Assert.NotNull(db);
39+
Assert.NotNull(context);
40+
41+
// Act & Assert
42+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
43+
{
44+
var cmd = new CreatePersonCommand { Name = "John" };
45+
await context.SendAsync(cmd, default);
46+
});
47+
}
48+
49+
[Fact]
50+
public async Task CommandRequestHandler_ReturnsSuccess()
51+
{
52+
// Arrange
53+
ServiceCollection services = new();
54+
services.AddWorkContext<CommandsDbContext>()
55+
.ConfigureDbContextPool((sp, builder) =>
56+
{
57+
SqliteConnection conn = new("DataSource=:memory:");
58+
conn.Open();
59+
builder.UseSqlite(conn);
60+
})
61+
.ConfigureRepositories(c => c.Add<Person>())
62+
.ConfigureCommands(c =>
63+
{
64+
c.AddHandler<CreatePersonHandler>();
65+
});
66+
67+
var root = services.BuildServiceProvider();
68+
var scope = root.CreateScope();
69+
var sp = scope.ServiceProvider;
70+
var db = sp.GetService<CommandsDbContext>();
71+
var context = sp.GetService<IWorkContext>();
72+
73+
Assert.NotNull(db);
74+
Assert.NotNull(context);
75+
76+
// Act
77+
var cmd = new CreatePersonCommand { Name = "John" };
78+
var result = await context.SendAsync(cmd, default);
79+
var hasProblems = result.HasProblems(out var problems);
80+
81+
// Assert
82+
Assert.False(hasProblems);
83+
Assert.NotNull(db.Set<Person>().FirstOrDefault(p => p.Name == "John"));
84+
}
85+
86+
[Fact]
87+
public async Task CommandRequestHandler_WithResponse_ReturnsExpectedResult()
88+
{
89+
// Arrange
90+
ServiceCollection services = new();
91+
services.AddWorkContext<CommandsDbContext>()
92+
.ConfigureDbContextPool((sp, builder) =>
93+
{
94+
SqliteConnection conn = new("DataSource=:memory:");
95+
conn.Open();
96+
builder.UseSqlite(conn);
97+
})
98+
.ConfigureRepositories(c => c.Add<Person>())
99+
.ConfigureCommands(c =>
100+
{
101+
c.AddHandler<CreatePersonWithResponseHandler>();
102+
});
103+
104+
var root = services.BuildServiceProvider();
105+
var scope = root.CreateScope();
106+
var sp = scope.ServiceProvider;
107+
var db = sp.GetService<CommandsDbContext>();
108+
var context = sp.GetService<IWorkContext>();
109+
110+
Assert.NotNull(db);
111+
Assert.NotNull(context);
112+
113+
// Act
114+
var cmd = new CreatePersonWithResponseCommand { Name = "Jane" };
115+
var result = await context.SendAsync(cmd, default);
116+
var hasProblems = result.HasProblemsOrGetValue(out var problems, out var person);
117+
118+
// Assert
119+
Assert.False(hasProblems);
120+
Assert.NotNull(person);
121+
Assert.Equal("Jane", person.Name);
122+
Assert.Equal(1, person.Id);
123+
}
124+
}
125+
126+
#region Test classes
127+
128+
class CommandsDbContext : DbContext
129+
{
130+
public CommandsDbContext(DbContextOptions<CommandsDbContext> options)
131+
: base(options)
132+
{
133+
Database.EnsureCreated();
134+
}
135+
136+
protected override void OnModelCreating(ModelBuilder modelBuilder)
137+
{
138+
base.OnModelCreating(modelBuilder);
139+
modelBuilder.Entity<Person>().ToTable("Persons");
140+
}
141+
}
142+
143+
class CreatePersonCommand : ICommandRequest
144+
{
145+
public string Name { get; set; } = null!;
146+
}
147+
148+
class CreatePersonWithResponseCommand : ICommandRequest<Person>
149+
{
150+
public string Name { get; set; } = null!;
151+
}
152+
153+
class CreatePersonHandler : ICommandHandler<CommandsDbContext, CreatePersonCommand>
154+
{
155+
public async Task<Result> HandleAsync(CreatePersonCommand request, IWorkContext<CommandsDbContext> context, CancellationToken ct = default)
156+
{
157+
context.Add(new Person { Name = request.Name });
158+
return await context.SaveAsync(ct);
159+
}
160+
}
161+
162+
class CreatePersonWithResponseHandler : ICommandHandler<CommandsDbContext, CreatePersonWithResponseCommand, Person>
163+
{
164+
public async Task<Result<Person>> HandleAsync(
165+
CreatePersonWithResponseCommand request,
166+
IWorkContext<CommandsDbContext> context,
167+
CancellationToken ct = default)
168+
{
169+
var person = new Person { Name = request.Name };
170+
context.Add(person);
171+
return await context.SaveAsync(ct).MapAsync(person);
172+
}
173+
}
174+
175+
#endregion

RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/IUnitOfWork.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@ namespace RoyalCode.UnitOfWork.EntityFramework;
1111
/// <typeparam name="TDbContext">The DbContext type.</typeparam>
1212
public interface IUnitOfWork<TDbContext> : IUnitOfWork
1313
where TDbContext: DbContext
14-
{ }
14+
{
15+
/// <summary>
16+
/// Gets the database context associated with the current operation.
17+
/// </summary>
18+
TDbContext Db { get; }
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using RoyalCode.SmartProblems;
2+
3+
namespace RoyalCode.WorkContext.Abstractions.Commands;
4+
5+
/// <summary>
6+
/// Defines a contract for dispatching command requests.
7+
/// </summary>
8+
public interface ICommandDispatcher
9+
{
10+
/// <summary>
11+
/// Sends a command request to the appropriate command handler for execution.
12+
/// </summary>
13+
/// <param name="request">The command request to be executed.</param>
14+
/// <param name="ct">A CancellationToken.</param>
15+
/// <returns>The result of the command execution.</returns>
16+
Task<Result> SendAsync(ICommandRequest request, CancellationToken ct = default);
17+
18+
/// <summary>
19+
/// Sends a command request that produces a response of type <typeparamref name="TResponse"/>
20+
/// to the appropriate command handler for execution.
21+
/// </summary>
22+
/// <typeparam name="TResponse"></typeparam>
23+
/// <param name="request">The command request to be executed.</param>
24+
/// <param name="ct">A CancellationToken.</param>
25+
/// <returns>The result of the command execution.</returns>
26+
Task<Result<TResponse>> SendAsync<TResponse>(ICommandRequest<TResponse> request, CancellationToken ct = default);
27+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace RoyalCode.WorkContext.Abstractions.Commands;
2+
3+
/// <summary>
4+
/// Represents a request to execute a command by some command handler.
5+
/// </summary>
6+
/// <remarks>
7+
/// This interface serves as a marker for command request objects, which are typically used in
8+
/// command-query separation (CQRS) patterns or similar architectures.
9+
/// <br />
10+
/// Implementations of this interface define the specific data and behavior required to execute a command.
11+
/// </remarks>
12+
public interface ICommandRequest { }
13+
14+
/// <summary>
15+
/// Represents a request that can be executed to produce a response of type <typeparamref name="TResponse"/>.
16+
/// </summary>
17+
/// <typeparam name="TResponse">The type of the response produced by the request.</typeparam>
18+
public interface ICommandRequest<TResponse> { }

RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Abstractions/IWorkContext.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using RoyalCode.Repositories.Abstractions;
33
using RoyalCode.SmartSearch;
44
using RoyalCode.UnitOfWork.Abstractions;
5+
using RoyalCode.WorkContext.Abstractions.Commands;
56
using RoyalCode.WorkContext.Abstractions.Querying;
67

78
namespace RoyalCode.WorkContext.Abstractions;
@@ -26,5 +27,13 @@ namespace RoyalCode.WorkContext.Abstractions;
2627
/// and the services that are provided is part of the persistence unit.
2728
/// </para>
2829
/// </summary>
29-
public interface IWorkContext : IUnitOfWork, IEntityManager, ISearchManager, IQueryDispatcher, IHintsContainer, IInfrastructureProvidesServices { }
30+
public interface IWorkContext :
31+
IUnitOfWork,
32+
IEntityManager,
33+
ISearchManager,
34+
IQueryDispatcher,
35+
ICommandDispatcher,
36+
IHintsContainer,
37+
IInfrastructureProvidesServices
38+
{ }
3039

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using System.Reflection;
3+
4+
namespace RoyalCode.WorkContext.EntityFramework.Commands.Configurations;
5+
6+
/// <summary>
7+
/// Provides access to the service collection for command configuration.
8+
/// </summary>
9+
public interface ICommandsConfigurer
10+
{
11+
/// <summary>
12+
/// Gets the service collection used for dependency injection.
13+
/// </summary>
14+
IServiceCollection Services { get; }
15+
16+
/// <summary>
17+
/// Registers all command handlers found in the specified assembly.
18+
/// </summary>
19+
/// <param name="assembly">The assembly to scan for command handlers.</param>
20+
/// <param name="lifetime">The service lifetime for the handlers. Default is <see cref="ServiceLifetime.Scoped"/>.</param>
21+
/// <returns>The current <see cref="ICommandsConfigurer"/> instance.</returns>
22+
ICommandsConfigurer AddHandlersFromAssembly(Assembly assembly, ServiceLifetime lifetime = ServiceLifetime.Scoped);
23+
24+
/// <summary>
25+
/// Registers all command handlers found in the assembly of the specified type.
26+
/// </summary>
27+
/// <typeparam name="T">A type from the target assembly.</typeparam>
28+
/// <param name="lifetime">The service lifetime for the handlers. Default is <see cref="ServiceLifetime.Scoped"/>.</param>
29+
/// <returns>The current <see cref="ICommandsConfigurer"/> instance.</returns>
30+
ICommandsConfigurer AddHandlersFromAssemblyOfType<T>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
31+
{
32+
return AddHandlersFromAssembly(typeof(T).Assembly, lifetime);
33+
}
34+
35+
/// <summary>
36+
/// Registers a command handler for the specified type.
37+
/// </summary>
38+
/// <typeparam name="THandler">The type of the command handler to register.</typeparam>
39+
/// <param name="lifetime">
40+
/// The service lifetime for the handler. Default is <see cref="ServiceLifetime.Transient"/>.
41+
/// </param>
42+
/// <returns>The current <see cref="ICommandsConfigurer"/> instance.</returns>
43+
ICommandsConfigurer AddHandler<THandler>(ServiceLifetime lifetime = ServiceLifetime.Transient);
44+
}

0 commit comments

Comments
 (0)