@polymorphism-tech/morph-spec 7.7.3 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +3 -2
- package/framework/agents.json +1 -1
- package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
- package/framework/rules/dotnet-templates.md +0 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
- package/framework/standards/backend/api/validation.md +2 -2
- package/framework/standards/backend/dotnet/scrutor.md +1 -3
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
- package/framework/standards/data/nosql/cache/redis.md +2 -2
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/README.md +0 -1
- package/framework/templates/REGISTRY.json +0 -21
- package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
- package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +4 -5
- package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
- package/framework/templates/code/dotnet/jobs/job.cs +15 -15
- package/framework/templates/code/dotnet/test.cs +72 -101
- package/package.json +1 -1
- package/src/utils/banner.js +53 -43
- package/framework/templates/code/dotnet/backend/repository.cs +0 -141
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
VSA + .NET 10 + EF Core + Neon (Postgres) + Microsoft Agent Framework + Next.js (App Router) + Tailwind + Framer Motion + Shadcn + Hangfire (quando necessário).
|
|
8
8
|
|
|
9
|
-
Padrões proibidos por padrão: `Guid.NewGuid()` (use `Guid.CreateVersion7()`), classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, SQL cru sem justificativa. Acesso a dados
|
|
9
|
+
Padrões proibidos por padrão: `Guid.NewGuid()` (use `Guid.CreateVersion7()`), classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
|
|
10
10
|
|
|
11
11
|
## Harness
|
|
12
12
|
|
|
@@ -256,27 +256,6 @@
|
|
|
256
256
|
"technology": null,
|
|
257
257
|
"validatePlaceholders": true
|
|
258
258
|
},
|
|
259
|
-
{
|
|
260
|
-
"id": "dotnet-backend-repository",
|
|
261
|
-
"kind": "template",
|
|
262
|
-
"name": ".NET Repository Class",
|
|
263
|
-
"path": "code/dotnet/backend/repository.cs",
|
|
264
|
-
"location": "framework",
|
|
265
|
-
"category": "code",
|
|
266
|
-
"phase": "implement",
|
|
267
|
-
"required": false,
|
|
268
|
-
"description": "C# repository class template with EF Core patterns",
|
|
269
|
-
"outputName": "{FeatureName}Repository.cs",
|
|
270
|
-
"placeholders": [],
|
|
271
|
-
"stackSpecific": false,
|
|
272
|
-
"engine": "handlebars",
|
|
273
|
-
"deprecated": false,
|
|
274
|
-
"technology": "dotnet",
|
|
275
|
-
"applicableStacks": [
|
|
276
|
-
"nextjs-neon"
|
|
277
|
-
],
|
|
278
|
-
"validatePlaceholders": true
|
|
279
|
-
},
|
|
280
259
|
{
|
|
281
260
|
"id": "dotnet-backend-test",
|
|
282
261
|
"kind": "template",
|
|
@@ -178,8 +178,8 @@ public static class {{pascalCase FEATURE_NAME}}Errors
|
|
|
178
178
|
namespace {{NAMESPACE}}.Features.{{pascalCase FEATURE_NAME}}Feature.Create{{pascalCase FEATURE_NAME}};
|
|
179
179
|
|
|
180
180
|
using {{NAMESPACE}}.Abstractions;
|
|
181
|
+
using {{NAMESPACE}}.Database;
|
|
181
182
|
using {{NAMESPACE}}.Entities;
|
|
182
|
-
using {{NAMESPACE}}.Repository;
|
|
183
183
|
|
|
184
184
|
// TODO: adicionar todos os campos do request (campos obrigatórios para criação)
|
|
185
185
|
public sealed record Create{{pascalCase FEATURE_NAME}}Request(/* string Name, decimal Price, ... */);
|
|
@@ -188,8 +188,7 @@ public sealed record Create{{pascalCase FEATURE_NAME}}Request(/* string Name, de
|
|
|
188
188
|
public sealed record Create{{pascalCase FEATURE_NAME}}Response(Guid Id /* , string Name, ... */);
|
|
189
189
|
|
|
190
190
|
public sealed class Create{{pascalCase FEATURE_NAME}}Handler(
|
|
191
|
-
|
|
192
|
-
IUnitOfWork _unitOfWork) : IHandler<Create{{pascalCase FEATURE_NAME}}Request, Result<Create{{pascalCase FEATURE_NAME}}Response>>
|
|
191
|
+
ApplicationDbContext _db) : IHandler<Create{{pascalCase FEATURE_NAME}}Request, Result<Create{{pascalCase FEATURE_NAME}}Response>>
|
|
193
192
|
{
|
|
194
193
|
public async Task<Result<Create{{pascalCase FEATURE_NAME}}Response>> HandleAsync(
|
|
195
194
|
Create{{pascalCase FEATURE_NAME}}Request command, CancellationToken cancellationToken)
|
|
@@ -202,8 +201,8 @@ public sealed class Create{{pascalCase FEATURE_NAME}}Handler(
|
|
|
202
201
|
// Price = command.Price,
|
|
203
202
|
};
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
await
|
|
204
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
205
|
+
await _db.SaveChangesAsync(cancellationToken);
|
|
207
206
|
|
|
208
207
|
return Result.Success(new Create{{pascalCase FEATURE_NAME}}Response(entity.Id /* , entity.Name, ... */));
|
|
209
208
|
}
|
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}Feature.{{pascalCase SLICE_NAME}};
|
|
2
|
-
|
|
3
|
-
using {{pascalCase PROJECT_NAME}}.Abstractions;
|
|
4
|
-
using {{pascalCase PROJECT_NAME}}.
|
|
5
|
-
using {{pascalCase PROJECT_NAME}}.
|
|
6
|
-
|
|
7
|
-
public sealed class {{pascalCase SLICE_NAME}}Handler(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
1
|
+
namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}Feature.{{pascalCase SLICE_NAME}};
|
|
2
|
+
|
|
3
|
+
using {{pascalCase PROJECT_NAME}}.Abstractions;
|
|
4
|
+
using {{pascalCase PROJECT_NAME}}.Database;
|
|
5
|
+
using {{pascalCase PROJECT_NAME}}.Entities;
|
|
6
|
+
|
|
7
|
+
public sealed class {{pascalCase SLICE_NAME}}Handler(
|
|
8
|
+
ApplicationDbContext _db) : IHandler<{{pascalCase SLICE_NAME}}Request, Result<{{pascalCase SLICE_NAME}}Response>>
|
|
9
|
+
{
|
|
10
|
+
public async Task<Result<{{pascalCase SLICE_NAME}}Response>> HandleAsync({{pascalCase SLICE_NAME}}Request command, CancellationToken cancellationToken)
|
|
11
|
+
{
|
|
12
|
+
var entity = new {{pascalCase FEATURE_NAME}}
|
|
13
|
+
{
|
|
14
|
+
Id = Guid.CreateVersion7(),
|
|
15
|
+
// TODO: Map command properties to entity
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
19
|
+
await _db.SaveChangesAsync(cancellationToken);
|
|
20
|
+
|
|
21
|
+
return Result.Success(new {{pascalCase SLICE_NAME}}Response(entity.Id));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
// ============================================================
|
|
2
|
-
// HANGFIRE JOB TEMPLATE (VSA Pattern —
|
|
2
|
+
// HANGFIRE JOB TEMPLATE (VSA Pattern — ApplicationDbContext direto)
|
|
3
3
|
// Generated by MORPH Framework
|
|
4
4
|
// ============================================================
|
|
5
5
|
|
|
6
6
|
using Hangfire;
|
|
7
|
+
using Microsoft.EntityFrameworkCore;
|
|
7
8
|
using Microsoft.Extensions.Logging;
|
|
8
|
-
using {{pascalCase PROJECT_NAME}}.
|
|
9
|
+
using {{pascalCase PROJECT_NAME}}.Database;
|
|
9
10
|
|
|
10
11
|
namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}.Jobs;
|
|
11
12
|
|
|
12
13
|
/// <summary>
|
|
13
14
|
/// Background job for processing {{pascalCase FEATURE_NAME}}.
|
|
14
15
|
/// Uses Hangfire for scheduling and retry handling.
|
|
15
|
-
/// Injects
|
|
16
|
+
/// Injects ApplicationDbContext directly (no service layer, no generic repository).
|
|
16
17
|
/// </summary>
|
|
17
18
|
public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
18
|
-
|
|
19
|
-
IUnitOfWork unitOfWork,
|
|
19
|
+
ApplicationDbContext db,
|
|
20
20
|
I{{pascalCase FEATURE_NAME}}AnalyzerAgent analyzer,
|
|
21
21
|
ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob
|
|
22
22
|
{
|
|
@@ -34,8 +34,8 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
|
34
34
|
|
|
35
35
|
try
|
|
36
36
|
{
|
|
37
|
-
// Get the entity directly from the
|
|
38
|
-
var entity = await
|
|
37
|
+
// Get the entity directly from the DbSet
|
|
38
|
+
var entity = await db.Set<{{pascalCase FEATURE_NAME}}>().FindAsync(new object[] { id }, cancellationToken);
|
|
39
39
|
if (entity is null)
|
|
40
40
|
{
|
|
41
41
|
logger.LogWarning("{{pascalCase FEATURE_NAME}} with ID {Id} not found, skipping", id);
|
|
@@ -57,10 +57,9 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
|
57
57
|
"{{pascalCase FEATURE_NAME}} {Id} analyzed. Confidence: {Confidence:P0}",
|
|
58
58
|
id, analysis.ConfidenceScore);
|
|
59
59
|
|
|
60
|
-
// Update entity
|
|
60
|
+
// Update the tracked entity and persist
|
|
61
61
|
entity.MarkAsCompleted(analysis);
|
|
62
|
-
await
|
|
63
|
-
await unitOfWork.CommitAsync(cancellationToken);
|
|
62
|
+
await db.SaveChangesAsync(cancellationToken);
|
|
64
63
|
|
|
65
64
|
logger.LogInformation("Completed {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
|
|
66
65
|
}
|
|
@@ -104,10 +103,10 @@ public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
|
104
103
|
|
|
105
104
|
/// <summary>
|
|
106
105
|
/// Batch job for processing multiple {{pascalCase FEATURE_NAME}}s.
|
|
107
|
-
/// Queries pending items via
|
|
106
|
+
/// Queries pending items via LINQ on the DbSet and enqueues individual jobs.
|
|
108
107
|
/// </summary>
|
|
109
108
|
public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
|
|
110
|
-
|
|
109
|
+
ApplicationDbContext db,
|
|
111
110
|
ILogger<{{pascalCase FEATURE_NAME}}BatchProcessorJob> logger)
|
|
112
111
|
{
|
|
113
112
|
/// <summary>
|
|
@@ -120,8 +119,9 @@ public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
|
|
|
120
119
|
{
|
|
121
120
|
logger.LogInformation("Starting batch processing for pending {{pascalCase FEATURE_NAME}}s");
|
|
122
121
|
|
|
123
|
-
var
|
|
124
|
-
|
|
122
|
+
var pending = await db.Set<{{pascalCase FEATURE_NAME}}>()
|
|
123
|
+
.Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending)
|
|
124
|
+
.ToListAsync(cancellationToken);
|
|
125
125
|
|
|
126
126
|
logger.LogInformation("Found {Count} pending {{pascalCase FEATURE_NAME}}s to process", pending.Count);
|
|
127
127
|
|
|
@@ -162,7 +162,7 @@ public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
|
|
|
162
162
|
// options.WorkerCount = Environment.ProcessorCount * 2;
|
|
163
163
|
// });
|
|
164
164
|
//
|
|
165
|
-
// // Register jobs (
|
|
165
|
+
// // Register jobs (ApplicationDbContext already registered via AddDbContext)
|
|
166
166
|
// builder.Services.AddScoped<I{{pascalCase FEATURE_NAME}}ProcessorJob, {{pascalCase FEATURE_NAME}}ProcessorJob>();
|
|
167
167
|
// builder.Services.AddScoped<{{pascalCase FEATURE_NAME}}BatchProcessorJob>();
|
|
168
168
|
//
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
using FluentAssertions;
|
|
7
7
|
using FluentValidation;
|
|
8
8
|
using FluentValidation.Results;
|
|
9
|
-
using Microsoft.
|
|
10
|
-
using
|
|
11
|
-
using {{pascalCase PROJECT_NAME}}.
|
|
9
|
+
using Microsoft.Data.Sqlite;
|
|
10
|
+
using Microsoft.EntityFrameworkCore;
|
|
11
|
+
using {{pascalCase PROJECT_NAME}}.Database;
|
|
12
|
+
using {{pascalCase PROJECT_NAME}}.Entities;
|
|
12
13
|
using Xunit;
|
|
13
14
|
|
|
14
15
|
namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_NAME}};
|
|
@@ -19,19 +20,31 @@ namespace {{pascalCase PROJECT_NAME}}.Tests.Unit.Features.{{pascalCase FEATURE_N
|
|
|
19
20
|
|
|
20
21
|
/// <summary>
|
|
21
22
|
/// Unit tests for {{pascalCase FEATURE_NAME}} handlers.
|
|
22
|
-
/// Tests handlers directly
|
|
23
|
+
/// Tests handlers directly against a real ApplicationDbContext backed by
|
|
24
|
+
/// in-memory SQLite (no mocks — DbSet is the repository, DbContext is the unit of work).
|
|
23
25
|
/// </summary>
|
|
24
|
-
public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
26
|
+
public class {{pascalCase FEATURE_NAME}}HandlerTests : IDisposable
|
|
25
27
|
{
|
|
26
|
-
private readonly
|
|
27
|
-
private readonly
|
|
28
|
-
private readonly ILogger<Create{{pascalCase FEATURE_NAME}}.Handler> _logger;
|
|
28
|
+
private readonly SqliteConnection _connection;
|
|
29
|
+
private readonly ApplicationDbContext _db;
|
|
29
30
|
|
|
30
31
|
public {{pascalCase FEATURE_NAME}}HandlerTests()
|
|
31
32
|
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
_connection = new SqliteConnection("DataSource=:memory:");
|
|
34
|
+
_connection.Open();
|
|
35
|
+
|
|
36
|
+
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
37
|
+
.UseSqlite(_connection)
|
|
38
|
+
.Options;
|
|
39
|
+
|
|
40
|
+
_db = new ApplicationDbContext(options);
|
|
41
|
+
_db.Database.EnsureCreated();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public void Dispose()
|
|
45
|
+
{
|
|
46
|
+
_db.Dispose();
|
|
47
|
+
_connection.Dispose();
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
#region Create Handler Tests
|
|
@@ -40,14 +53,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
40
53
|
public async Task Create_HandleAsync_WithValidCommand_ReturnsSuccessResult()
|
|
41
54
|
{
|
|
42
55
|
// Arrange
|
|
43
|
-
var command = new Create{{pascalCase FEATURE_NAME}}
|
|
44
|
-
var handler = new Create{{pascalCase FEATURE_NAME}}
|
|
45
|
-
|
|
46
|
-
_repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
|
|
47
|
-
.Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
|
|
48
|
-
|
|
49
|
-
_unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
|
|
50
|
-
.Returns(1);
|
|
56
|
+
var command = new Create{{pascalCase FEATURE_NAME}}Request("Test {{pascalCase FEATURE_NAME}}");
|
|
57
|
+
var handler = new Create{{pascalCase FEATURE_NAME}}Handler(_db);
|
|
51
58
|
|
|
52
59
|
// Act
|
|
53
60
|
var result = await handler.HandleAsync(command, CancellationToken.None);
|
|
@@ -55,31 +62,9 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
55
62
|
// Assert
|
|
56
63
|
result.IsSuccess.Should().BeTrue();
|
|
57
64
|
result.Value.Should().NotBeNull();
|
|
58
|
-
result.Value.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
|
|
59
65
|
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
[Fact]
|
|
65
|
-
public async Task Create_HandleAsync_WhenCommitFails_ReturnsFailureResult()
|
|
66
|
-
{
|
|
67
|
-
// Arrange
|
|
68
|
-
var command = new Create{{pascalCase FEATURE_NAME}}.Command("Test {{pascalCase FEATURE_NAME}}");
|
|
69
|
-
var handler = new Create{{pascalCase FEATURE_NAME}}.Handler(_repository, _unitOfWork, _logger);
|
|
70
|
-
|
|
71
|
-
_repository.AddAsync(Arg.Any<{{pascalCase FEATURE_NAME}}>(), Arg.Any<CancellationToken>())
|
|
72
|
-
.Returns(callInfo => callInfo.Arg<{{pascalCase FEATURE_NAME}}>());
|
|
73
|
-
|
|
74
|
-
_unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
|
|
75
|
-
.ThrowsAsync(new InvalidOperationException("Database error"));
|
|
76
|
-
|
|
77
|
-
// Act
|
|
78
|
-
var act = async () => await handler.HandleAsync(command, CancellationToken.None);
|
|
79
|
-
|
|
80
|
-
// Assert
|
|
81
|
-
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
82
|
-
.WithMessage("Database error");
|
|
66
|
+
var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync();
|
|
67
|
+
persisted.Name.Should().Be("Test {{pascalCase FEATURE_NAME}}");
|
|
83
68
|
}
|
|
84
69
|
|
|
85
70
|
#endregion
|
|
@@ -90,12 +75,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
90
75
|
public async Task GetById_HandleAsync_WithValidId_ReturnsSuccessResult()
|
|
91
76
|
{
|
|
92
77
|
// Arrange
|
|
93
|
-
var
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
var entity = CreateTestEntity("Test {{pascalCase FEATURE_NAME}}");
|
|
79
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
80
|
+
await _db.SaveChangesAsync();
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(entity.Id);
|
|
83
|
+
var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
|
|
99
84
|
|
|
100
85
|
// Act
|
|
101
86
|
var result = await handler.HandleAsync(query, CancellationToken.None);
|
|
@@ -110,11 +95,8 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
110
95
|
public async Task GetById_HandleAsync_WithInvalidId_ReturnsFailureResult()
|
|
111
96
|
{
|
|
112
97
|
// Arrange
|
|
113
|
-
var query = new
|
|
114
|
-
var handler = new
|
|
115
|
-
|
|
116
|
-
_repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
|
|
117
|
-
.Returns(({{pascalCase FEATURE_NAME}}?)null);
|
|
98
|
+
var query = new Get{{pascalCase FEATURE_NAME}}ByIdRequest(Guid.CreateVersion7());
|
|
99
|
+
var handler = new Get{{pascalCase FEATURE_NAME}}ByIdHandler(_db);
|
|
118
100
|
|
|
119
101
|
// Act
|
|
120
102
|
var result = await handler.HandleAsync(query, CancellationToken.None);
|
|
@@ -131,15 +113,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
131
113
|
public async Task Update_HandleAsync_WithValidCommand_ReturnsSuccessResult()
|
|
132
114
|
{
|
|
133
115
|
// Arrange
|
|
134
|
-
var
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
_repository.GetByIdAsync(1, Arg.Any<CancellationToken>())
|
|
139
|
-
.Returns(entity);
|
|
116
|
+
var entity = CreateTestEntity("Original Name");
|
|
117
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
118
|
+
await _db.SaveChangesAsync();
|
|
140
119
|
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
var command = new Update{{pascalCase FEATURE_NAME}}Request(entity.Id, "Updated Name");
|
|
121
|
+
var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
|
|
143
122
|
|
|
144
123
|
// Act
|
|
145
124
|
var result = await handler.HandleAsync(command, CancellationToken.None);
|
|
@@ -147,26 +126,22 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
147
126
|
// Assert
|
|
148
127
|
result.IsSuccess.Should().BeTrue();
|
|
149
128
|
|
|
150
|
-
await
|
|
151
|
-
|
|
129
|
+
var persisted = await _db.Set<{{pascalCase FEATURE_NAME}}>().SingleAsync(x => x.Id == entity.Id);
|
|
130
|
+
persisted.Name.Should().Be("Updated Name");
|
|
152
131
|
}
|
|
153
132
|
|
|
154
133
|
[Fact]
|
|
155
134
|
public async Task Update_HandleAsync_WithNonExistentId_ReturnsFailureResult()
|
|
156
135
|
{
|
|
157
136
|
// Arrange
|
|
158
|
-
var command = new Update{{pascalCase FEATURE_NAME}}.
|
|
159
|
-
var handler = new Update{{pascalCase FEATURE_NAME}}
|
|
160
|
-
|
|
161
|
-
_repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
|
|
162
|
-
.Returns(({{pascalCase FEATURE_NAME}}?)null);
|
|
137
|
+
var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated");
|
|
138
|
+
var handler = new Update{{pascalCase FEATURE_NAME}}Handler(_db);
|
|
163
139
|
|
|
164
140
|
// Act
|
|
165
141
|
var result = await handler.HandleAsync(command, CancellationToken.None);
|
|
166
142
|
|
|
167
143
|
// Assert
|
|
168
144
|
result.IsSuccess.Should().BeFalse();
|
|
169
|
-
await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
|
|
170
145
|
}
|
|
171
146
|
|
|
172
147
|
#endregion
|
|
@@ -177,15 +152,12 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
177
152
|
public async Task Delete_HandleAsync_WithValidId_ReturnsSuccessResult()
|
|
178
153
|
{
|
|
179
154
|
// Arrange
|
|
180
|
-
var
|
|
181
|
-
|
|
182
|
-
|
|
155
|
+
var entity = CreateTestEntity("To Delete");
|
|
156
|
+
_db.Set<{{pascalCase FEATURE_NAME}}>().Add(entity);
|
|
157
|
+
await _db.SaveChangesAsync();
|
|
183
158
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
_unitOfWork.CommitAsync(Arg.Any<CancellationToken>())
|
|
188
|
-
.Returns(1);
|
|
159
|
+
var command = new Delete{{pascalCase FEATURE_NAME}}Request(entity.Id);
|
|
160
|
+
var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
|
|
189
161
|
|
|
190
162
|
// Act
|
|
191
163
|
var result = await handler.HandleAsync(command, CancellationToken.None);
|
|
@@ -193,36 +165,35 @@ public class {{pascalCase FEATURE_NAME}}HandlerTests
|
|
|
193
165
|
// Assert
|
|
194
166
|
result.IsSuccess.Should().BeTrue();
|
|
195
167
|
|
|
196
|
-
await
|
|
197
|
-
await _unitOfWork.Received(1).CommitAsync(Arg.Any<CancellationToken>());
|
|
168
|
+
(await _db.Set<{{pascalCase FEATURE_NAME}}>().AnyAsync(x => x.Id == entity.Id)).Should().BeFalse();
|
|
198
169
|
}
|
|
199
170
|
|
|
200
171
|
[Fact]
|
|
201
172
|
public async Task Delete_HandleAsync_WithNonExistentId_ReturnsFailureResult()
|
|
202
173
|
{
|
|
203
174
|
// Arrange
|
|
204
|
-
var command = new Delete{{pascalCase FEATURE_NAME}}.
|
|
205
|
-
var handler = new Delete{{pascalCase FEATURE_NAME}}
|
|
206
|
-
|
|
207
|
-
_repository.GetByIdAsync(999, Arg.Any<CancellationToken>())
|
|
208
|
-
.Returns(({{pascalCase FEATURE_NAME}}?)null);
|
|
175
|
+
var command = new Delete{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7());
|
|
176
|
+
var handler = new Delete{{pascalCase FEATURE_NAME}}Handler(_db);
|
|
209
177
|
|
|
210
178
|
// Act
|
|
211
179
|
var result = await handler.HandleAsync(command, CancellationToken.None);
|
|
212
180
|
|
|
213
181
|
// Assert
|
|
214
182
|
result.IsSuccess.Should().BeFalse();
|
|
215
|
-
await _unitOfWork.DidNotReceive().CommitAsync(Arg.Any<CancellationToken>());
|
|
216
183
|
}
|
|
217
184
|
|
|
218
185
|
#endregion
|
|
219
186
|
|
|
220
187
|
#region Helper Methods
|
|
221
188
|
|
|
222
|
-
private static {{pascalCase FEATURE_NAME}} CreateTestEntity(
|
|
189
|
+
private static {{pascalCase FEATURE_NAME}} CreateTestEntity(string name)
|
|
223
190
|
{
|
|
224
|
-
return {{pascalCase FEATURE_NAME}}
|
|
225
|
-
|
|
191
|
+
return new {{pascalCase FEATURE_NAME}}
|
|
192
|
+
{
|
|
193
|
+
Id = Guid.CreateVersion7(),
|
|
194
|
+
Name = name,
|
|
195
|
+
// TODO: preencher demais campos obrigatórios da entidade
|
|
196
|
+
};
|
|
226
197
|
}
|
|
227
198
|
|
|
228
199
|
#endregion
|
|
@@ -244,8 +215,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
|
|
|
244
215
|
public async Task CreateValidator_WithValidCommand_PassesValidation()
|
|
245
216
|
{
|
|
246
217
|
// Arrange
|
|
247
|
-
var validator = new Create{{pascalCase FEATURE_NAME}}
|
|
248
|
-
var command = new Create{{pascalCase FEATURE_NAME}}
|
|
218
|
+
var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
|
|
219
|
+
var command = new Create{{pascalCase FEATURE_NAME}}Request("Valid Name");
|
|
249
220
|
|
|
250
221
|
// Act
|
|
251
222
|
ValidationResult result = await validator.ValidateAsync(command);
|
|
@@ -262,30 +233,30 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
|
|
|
262
233
|
public async Task CreateValidator_WithInvalidName_FailsValidation(string? name)
|
|
263
234
|
{
|
|
264
235
|
// Arrange
|
|
265
|
-
var validator = new Create{{pascalCase FEATURE_NAME}}
|
|
266
|
-
var command = new Create{{pascalCase FEATURE_NAME}}
|
|
236
|
+
var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
|
|
237
|
+
var command = new Create{{pascalCase FEATURE_NAME}}Request(name!);
|
|
267
238
|
|
|
268
239
|
// Act
|
|
269
240
|
ValidationResult result = await validator.ValidateAsync(command);
|
|
270
241
|
|
|
271
242
|
// Assert
|
|
272
243
|
result.IsValid.Should().BeFalse();
|
|
273
|
-
result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.
|
|
244
|
+
result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
|
|
274
245
|
}
|
|
275
246
|
|
|
276
247
|
[Fact]
|
|
277
248
|
public async Task CreateValidator_WithNameExceedingMaxLength_FailsValidation()
|
|
278
249
|
{
|
|
279
250
|
// Arrange
|
|
280
|
-
var validator = new Create{{pascalCase FEATURE_NAME}}
|
|
281
|
-
var command = new Create{{pascalCase FEATURE_NAME}}
|
|
251
|
+
var validator = new Create{{pascalCase FEATURE_NAME}}Validator();
|
|
252
|
+
var command = new Create{{pascalCase FEATURE_NAME}}Request(new string('A', 201));
|
|
282
253
|
|
|
283
254
|
// Act
|
|
284
255
|
ValidationResult result = await validator.ValidateAsync(command);
|
|
285
256
|
|
|
286
257
|
// Assert
|
|
287
258
|
result.IsValid.Should().BeFalse();
|
|
288
|
-
result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}.
|
|
259
|
+
result.Errors.Should().Contain(e => e.PropertyName == nameof(Create{{pascalCase FEATURE_NAME}}Request.Name));
|
|
289
260
|
}
|
|
290
261
|
|
|
291
262
|
#endregion
|
|
@@ -296,8 +267,8 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
|
|
|
296
267
|
public async Task UpdateValidator_WithValidCommand_PassesValidation()
|
|
297
268
|
{
|
|
298
269
|
// Arrange
|
|
299
|
-
var validator = new Update{{pascalCase FEATURE_NAME}}
|
|
300
|
-
var command = new Update{{pascalCase FEATURE_NAME}}.
|
|
270
|
+
var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
|
|
271
|
+
var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.CreateVersion7(), "Updated Name");
|
|
301
272
|
|
|
302
273
|
// Act
|
|
303
274
|
ValidationResult result = await validator.ValidateAsync(command);
|
|
@@ -310,15 +281,15 @@ public class {{pascalCase FEATURE_NAME}}ValidatorTests
|
|
|
310
281
|
public async Task UpdateValidator_WithInvalidId_FailsValidation()
|
|
311
282
|
{
|
|
312
283
|
// Arrange
|
|
313
|
-
var validator = new Update{{pascalCase FEATURE_NAME}}
|
|
314
|
-
var command = new Update{{pascalCase FEATURE_NAME}}.
|
|
284
|
+
var validator = new Update{{pascalCase FEATURE_NAME}}Validator();
|
|
285
|
+
var command = new Update{{pascalCase FEATURE_NAME}}Request(Guid.Empty, "Updated Name");
|
|
315
286
|
|
|
316
287
|
// Act
|
|
317
288
|
ValidationResult result = await validator.ValidateAsync(command);
|
|
318
289
|
|
|
319
290
|
// Assert
|
|
320
291
|
result.IsValid.Should().BeFalse();
|
|
321
|
-
result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}.
|
|
292
|
+
result.Errors.Should().Contain(e => e.PropertyName == nameof(Update{{pascalCase FEATURE_NAME}}Request.Id));
|
|
322
293
|
}
|
|
323
294
|
|
|
324
295
|
#endregion
|