@dennisrongo/skills 0.1.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/LICENSE +21 -0
- package/README.md +230 -0
- package/bin/claude-skills.js +189 -0
- package/lib/install.js +111 -0
- package/lib/list.js +63 -0
- package/lib/paths.js +39 -0
- package/lib/remove.js +52 -0
- package/lib/skills.js +121 -0
- package/package.json +48 -0
- package/skills/_template/SKILL.md +34 -0
- package/skills/conventional-commits/SKILL.md +136 -0
- package/skills/diagnose/SKILL.md +140 -0
- package/skills/dotnet-onion-api/SKILL.md +267 -0
- package/skills/dotnet-onion-api/references/anti-patterns.md +155 -0
- package/skills/dotnet-onion-api/references/solution-layout.md +113 -0
- package/skills/dotnet-onion-api/references/templates/appsettings.md +75 -0
- package/skills/dotnet-onion-api/references/templates/base-controller.cs.md +90 -0
- package/skills/dotnet-onion-api/references/templates/csproj-files.md +178 -0
- package/skills/dotnet-onion-api/references/templates/dbcontext.cs.md +149 -0
- package/skills/dotnet-onion-api/references/templates/exception-middleware.cs.md +101 -0
- package/skills/dotnet-onion-api/references/templates/feature-slice.md +349 -0
- package/skills/dotnet-onion-api/references/templates/program-cs.md +171 -0
- package/skills/dotnet-onion-api/references/templates/worker-program.cs.md +166 -0
- package/skills/grill-with-docs/SKILL.md +203 -0
- package/skills/handoff/SKILL.md +155 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +121 -0
- package/skills/nextjs-app-router/SKILL.md +328 -0
- package/skills/nextjs-app-router/references/anti-patterns.md +203 -0
- package/skills/nextjs-app-router/references/folder-layout.md +151 -0
- package/skills/nextjs-app-router/references/good-patterns.md +212 -0
- package/skills/nextjs-app-router/references/templates/api-base.md +62 -0
- package/skills/nextjs-app-router/references/templates/api-slice.md +75 -0
- package/skills/nextjs-app-router/references/templates/auth-slice.md +95 -0
- package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +94 -0
- package/skills/nextjs-app-router/references/templates/components-json.md +41 -0
- package/skills/nextjs-app-router/references/templates/env-and-utils.md +110 -0
- package/skills/nextjs-app-router/references/templates/eslint-prettier.md +82 -0
- package/skills/nextjs-app-router/references/templates/feature-slice.md +184 -0
- package/skills/nextjs-app-router/references/templates/form-with-zod.md +192 -0
- package/skills/nextjs-app-router/references/templates/middleware.md +88 -0
- package/skills/nextjs-app-router/references/templates/next-config.md +42 -0
- package/skills/nextjs-app-router/references/templates/package.md +99 -0
- package/skills/nextjs-app-router/references/templates/providers-and-store.md +79 -0
- package/skills/nextjs-app-router/references/templates/root-layout.md +146 -0
- package/skills/nextjs-app-router/references/templates/route-group-layouts.md +90 -0
- package/skills/nextjs-app-router/references/templates/tailwind-config.md +89 -0
- package/skills/nextjs-app-router/references/templates/testing.md +137 -0
- package/skills/nextjs-app-router/references/templates/tsconfig.md +40 -0
- package/skills/plan-and-build/SKILL.md +214 -0
- package/skills/pr-review/SKILL.md +132 -0
- package/skills/write-a-skill/SKILL.md +191 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `src/{{Solution}}.Api/Controllers/BaseController.cs`
|
|
2
|
+
|
|
3
|
+
```csharp
|
|
4
|
+
using {{Solution}}.Application.Common;
|
|
5
|
+
using Microsoft.AspNetCore.Authorization;
|
|
6
|
+
using Microsoft.AspNetCore.Mvc;
|
|
7
|
+
|
|
8
|
+
namespace {{Solution}}.Api.Controllers;
|
|
9
|
+
|
|
10
|
+
[ApiController]
|
|
11
|
+
[Route("api/[controller]")]
|
|
12
|
+
[Authorize]
|
|
13
|
+
[Produces("application/json")]
|
|
14
|
+
public abstract class BaseController : ControllerBase
|
|
15
|
+
{
|
|
16
|
+
protected readonly IUserContext UserContext;
|
|
17
|
+
|
|
18
|
+
protected BaseController(IUserContext userContext)
|
|
19
|
+
{
|
|
20
|
+
UserContext = userContext;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
# `src/{{Solution}}.Application/Common/IUserContext.cs`
|
|
26
|
+
|
|
27
|
+
```csharp
|
|
28
|
+
namespace {{Solution}}.Application.Common;
|
|
29
|
+
|
|
30
|
+
/// Provides per-request user identity. Resolved per scope.
|
|
31
|
+
public interface IUserContext
|
|
32
|
+
{
|
|
33
|
+
int UserId { get; }
|
|
34
|
+
string UserName { get; }
|
|
35
|
+
string? ClientId { get; }
|
|
36
|
+
bool IsAuthenticated { get; }
|
|
37
|
+
IReadOnlyCollection<string> Roles { get; }
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
# `src/{{Solution}}.Infrastructure/Auth/UserContext.cs`
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
using System.Security.Claims;
|
|
45
|
+
using {{Solution}}.Application.Common;
|
|
46
|
+
using Microsoft.AspNetCore.Http;
|
|
47
|
+
|
|
48
|
+
namespace {{Solution}}.Infrastructure.Auth;
|
|
49
|
+
|
|
50
|
+
public sealed class UserContext : IUserContext
|
|
51
|
+
{
|
|
52
|
+
private readonly IHttpContextAccessor _accessor;
|
|
53
|
+
|
|
54
|
+
public UserContext(IHttpContextAccessor accessor) => _accessor = accessor;
|
|
55
|
+
|
|
56
|
+
private ClaimsPrincipal? User => _accessor.HttpContext?.User;
|
|
57
|
+
|
|
58
|
+
public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;
|
|
59
|
+
|
|
60
|
+
public int UserId =>
|
|
61
|
+
int.TryParse(User?.FindFirstValue(ClaimTypes.NameIdentifier), out var id) ? id : 0;
|
|
62
|
+
|
|
63
|
+
public string UserName => User?.FindFirstValue(ClaimTypes.Name) ?? string.Empty;
|
|
64
|
+
|
|
65
|
+
public string? ClientId => User?.FindFirstValue("client_id");
|
|
66
|
+
|
|
67
|
+
public IReadOnlyCollection<string> Roles =>
|
|
68
|
+
User?.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray() ?? Array.Empty<string>();
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
# `src/{{Solution}}.Api/Models/ApiException.cs`
|
|
73
|
+
|
|
74
|
+
```csharp
|
|
75
|
+
namespace {{Solution}}.Api.Models;
|
|
76
|
+
|
|
77
|
+
public sealed record ApiException(int StatusCode, string Message, string? Detail = null);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
# `src/{{Solution}}.Api/Models/ApiValidationErrorResponse.cs`
|
|
81
|
+
|
|
82
|
+
```csharp
|
|
83
|
+
namespace {{Solution}}.Api.Models;
|
|
84
|
+
|
|
85
|
+
public sealed class ApiValidationErrorResponse
|
|
86
|
+
{
|
|
87
|
+
public string Message { get; set; } = string.Empty;
|
|
88
|
+
public string[] Errors { get; set; } = Array.Empty<string>();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# `.csproj` templates
|
|
2
|
+
|
|
3
|
+
⚠️ **Do not paste these as-is.** The `<TargetFramework>` and every `<PackageReference Version="…">` must be resolved at scaffold time (see SKILL.md Step 1). Treat the version strings here as placeholders shown for shape only.
|
|
4
|
+
|
|
5
|
+
## `Directory.Build.props` (solution root)
|
|
6
|
+
|
|
7
|
+
```xml
|
|
8
|
+
<Project>
|
|
9
|
+
<PropertyGroup>
|
|
10
|
+
<LangVersion>latest</LangVersion>
|
|
11
|
+
<Nullable>enable</Nullable>
|
|
12
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
13
|
+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
14
|
+
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
|
15
|
+
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
|
16
|
+
</PropertyGroup>
|
|
17
|
+
</Project>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## `src/{{Solution}}.Domain/{{Solution}}.Domain.csproj`
|
|
21
|
+
|
|
22
|
+
```xml
|
|
23
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
24
|
+
<PropertyGroup>
|
|
25
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
26
|
+
</PropertyGroup>
|
|
27
|
+
<!-- ZERO ProjectReferences. Domain depends on nothing. -->
|
|
28
|
+
</Project>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## `src/{{Solution}}.Application/{{Solution}}.Application.csproj`
|
|
32
|
+
|
|
33
|
+
```xml
|
|
34
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
35
|
+
<PropertyGroup>
|
|
36
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
37
|
+
</PropertyGroup>
|
|
38
|
+
<ItemGroup>
|
|
39
|
+
<ProjectReference Include="..\{{Solution}}.Domain\{{Solution}}.Domain.csproj" />
|
|
40
|
+
</ItemGroup>
|
|
41
|
+
<ItemGroup>
|
|
42
|
+
<PackageReference Include="AutoMapper" Version="{{LATEST}}" />
|
|
43
|
+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="{{LATEST}}" />
|
|
44
|
+
<PackageReference Include="Scrutor" Version="{{LATEST}}" />
|
|
45
|
+
</ItemGroup>
|
|
46
|
+
</Project>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## `src/{{Solution}}.Infrastructure/{{Solution}}.Infrastructure.csproj`
|
|
50
|
+
|
|
51
|
+
```xml
|
|
52
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
53
|
+
<PropertyGroup>
|
|
54
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
55
|
+
</PropertyGroup>
|
|
56
|
+
<ItemGroup>
|
|
57
|
+
<ProjectReference Include="..\{{Solution}}.Application\{{Solution}}.Application.csproj" />
|
|
58
|
+
<ProjectReference Include="..\{{Solution}}.Domain\{{Solution}}.Domain.csproj" />
|
|
59
|
+
</ItemGroup>
|
|
60
|
+
<ItemGroup>
|
|
61
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="{{LATEST}}" />
|
|
62
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="{{LATEST}}" />
|
|
63
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="{{LATEST}}">
|
|
64
|
+
<PrivateAssets>all</PrivateAssets>
|
|
65
|
+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
66
|
+
</PackageReference>
|
|
67
|
+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="{{LATEST}}" />
|
|
68
|
+
<PackageReference Include="Microsoft.Extensions.Http" Version="{{LATEST}}" />
|
|
69
|
+
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="{{LATEST}}" />
|
|
70
|
+
</ItemGroup>
|
|
71
|
+
</Project>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## `src/{{Solution}}.Api/{{Solution}}.Api.csproj`
|
|
75
|
+
|
|
76
|
+
```xml
|
|
77
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
78
|
+
<PropertyGroup>
|
|
79
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
80
|
+
<UserSecretsId>{{NEW_GUID}}</UserSecretsId>
|
|
81
|
+
</PropertyGroup>
|
|
82
|
+
<ItemGroup>
|
|
83
|
+
<ProjectReference Include="..\{{Solution}}.Application\{{Solution}}.Application.csproj" />
|
|
84
|
+
<ProjectReference Include="..\{{Solution}}.Infrastructure\{{Solution}}.Infrastructure.csproj" />
|
|
85
|
+
</ItemGroup>
|
|
86
|
+
<ItemGroup>
|
|
87
|
+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="{{LATEST}}" />
|
|
88
|
+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="{{LATEST}}" />
|
|
89
|
+
<PackageReference Include="Swashbuckle.AspNetCore" Version="{{LATEST}}" />
|
|
90
|
+
</ItemGroup>
|
|
91
|
+
</Project>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## `src/{{Solution}}.Workers.{{Worker}}/{{Solution}}.Workers.{{Worker}}.csproj`
|
|
95
|
+
|
|
96
|
+
```xml
|
|
97
|
+
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
|
98
|
+
<PropertyGroup>
|
|
99
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
100
|
+
<OutputType>Exe</OutputType>
|
|
101
|
+
</PropertyGroup>
|
|
102
|
+
<ItemGroup>
|
|
103
|
+
<ProjectReference Include="..\{{Solution}}.Application\{{Solution}}.Application.csproj" />
|
|
104
|
+
<ProjectReference Include="..\{{Solution}}.Infrastructure\{{Solution}}.Infrastructure.csproj" />
|
|
105
|
+
</ItemGroup>
|
|
106
|
+
<ItemGroup>
|
|
107
|
+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="{{LATEST}}" />
|
|
108
|
+
<!-- Add only the SDK clients this worker actually needs: -->
|
|
109
|
+
<!-- <PackageReference Include="Azure.Messaging.ServiceBus" Version="{{LATEST}}" /> -->
|
|
110
|
+
<!-- <PackageReference Include="Azure.Storage.Queues" Version="{{LATEST}}" /> -->
|
|
111
|
+
</ItemGroup>
|
|
112
|
+
<ItemGroup>
|
|
113
|
+
<None Update="appsettings.json"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None>
|
|
114
|
+
<None Update="appsettings.Development.json"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None>
|
|
115
|
+
</ItemGroup>
|
|
116
|
+
</Project>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## `tests/{{Solution}}.UnitTests/{{Solution}}.UnitTests.csproj`
|
|
120
|
+
|
|
121
|
+
```xml
|
|
122
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
123
|
+
<PropertyGroup>
|
|
124
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
125
|
+
<IsPackable>false</IsPackable>
|
|
126
|
+
</PropertyGroup>
|
|
127
|
+
<ItemGroup>
|
|
128
|
+
<ProjectReference Include="..\..\src\{{Solution}}.Application\{{Solution}}.Application.csproj" />
|
|
129
|
+
<ProjectReference Include="..\..\src\{{Solution}}.Domain\{{Solution}}.Domain.csproj" />
|
|
130
|
+
</ItemGroup>
|
|
131
|
+
<ItemGroup>
|
|
132
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="{{LATEST}}" />
|
|
133
|
+
<PackageReference Include="xunit" Version="{{LATEST}}" />
|
|
134
|
+
<PackageReference Include="xunit.runner.visualstudio" Version="{{LATEST}}" />
|
|
135
|
+
<PackageReference Include="NSubstitute" Version="{{LATEST}}" />
|
|
136
|
+
<PackageReference Include="FluentAssertions" Version="{{LATEST}}" />
|
|
137
|
+
<PackageReference Include="AutoMapper" Version="{{LATEST}}" />
|
|
138
|
+
</ItemGroup>
|
|
139
|
+
</Project>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## `tests/{{Solution}}.IntegrationTests/{{Solution}}.IntegrationTests.csproj`
|
|
143
|
+
|
|
144
|
+
Only generate if the user explicitly opts in.
|
|
145
|
+
|
|
146
|
+
```xml
|
|
147
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
148
|
+
<PropertyGroup>
|
|
149
|
+
<TargetFramework>{{TFM}}</TargetFramework>
|
|
150
|
+
<IsPackable>false</IsPackable>
|
|
151
|
+
</PropertyGroup>
|
|
152
|
+
<ItemGroup>
|
|
153
|
+
<ProjectReference Include="..\..\src\{{Solution}}.Api\{{Solution}}.Api.csproj" />
|
|
154
|
+
</ItemGroup>
|
|
155
|
+
<ItemGroup>
|
|
156
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="{{LATEST}}" />
|
|
157
|
+
<PackageReference Include="xunit" Version="{{LATEST}}" />
|
|
158
|
+
<PackageReference Include="xunit.runner.visualstudio" Version="{{LATEST}}" />
|
|
159
|
+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="{{LATEST}}" />
|
|
160
|
+
<PackageReference Include="Testcontainers.MsSql" Version="{{LATEST}}" />
|
|
161
|
+
<PackageReference Include="FluentAssertions" Version="{{LATEST}}" />
|
|
162
|
+
</ItemGroup>
|
|
163
|
+
</Project>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## How to resolve `{{LATEST}}` at scaffold time
|
|
167
|
+
|
|
168
|
+
For each package, call:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
mcp__plugin_context7_context7__resolve-library-id → query-docs
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
with the package id (e.g. `Microsoft.EntityFrameworkCore.SqlServer`). If context7 can't resolve, fall back to `WebFetch` of `https://www.nuget.org/packages/{Package}` and parse the latest stable version (skip pre-release unless the user opts in).
|
|
175
|
+
|
|
176
|
+
Then substitute every `{{LATEST}}` with the resolved version string before writing the `.csproj`.
|
|
177
|
+
|
|
178
|
+
State the resolved version table back to the user before writing files.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# EF Core DbContext + Application ports + Infrastructure DI
|
|
2
|
+
|
|
3
|
+
## `src/{{Solution}}.Infrastructure/Persistence/AppDbContext.cs`
|
|
4
|
+
|
|
5
|
+
```csharp
|
|
6
|
+
using {{Solution}}.Application.Common;
|
|
7
|
+
using Microsoft.EntityFrameworkCore;
|
|
8
|
+
using System.Reflection;
|
|
9
|
+
|
|
10
|
+
namespace {{Solution}}.Infrastructure.Persistence;
|
|
11
|
+
|
|
12
|
+
public sealed class AppDbContext : DbContext
|
|
13
|
+
{
|
|
14
|
+
private readonly IUserContext _userContext;
|
|
15
|
+
|
|
16
|
+
public AppDbContext(DbContextOptions<AppDbContext> options, IUserContext userContext)
|
|
17
|
+
: base(options)
|
|
18
|
+
{
|
|
19
|
+
_userContext = userContext;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add DbSet<T> properties per feature. Example:
|
|
23
|
+
// public DbSet<Customer> Customers => Set<Customer>();
|
|
24
|
+
|
|
25
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
26
|
+
{
|
|
27
|
+
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
|
28
|
+
base.OnModelCreating(modelBuilder);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
|
|
32
|
+
{
|
|
33
|
+
// Hook: stamp audit columns from _userContext here if entities implement IAuditable.
|
|
34
|
+
return base.SaveChangesAsync(ct);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `src/{{Solution}}.Application/Common/IUnitOfWork.cs`
|
|
40
|
+
|
|
41
|
+
```csharp
|
|
42
|
+
namespace {{Solution}}.Application.Common;
|
|
43
|
+
|
|
44
|
+
public interface IUnitOfWork
|
|
45
|
+
{
|
|
46
|
+
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## `src/{{Solution}}.Infrastructure/Persistence/UnitOfWork.cs`
|
|
51
|
+
|
|
52
|
+
```csharp
|
|
53
|
+
using {{Solution}}.Application.Common;
|
|
54
|
+
|
|
55
|
+
namespace {{Solution}}.Infrastructure.Persistence;
|
|
56
|
+
|
|
57
|
+
public sealed class UnitOfWork : IUnitOfWork
|
|
58
|
+
{
|
|
59
|
+
private readonly AppDbContext _db;
|
|
60
|
+
public UnitOfWork(AppDbContext db) => _db = db;
|
|
61
|
+
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## `src/{{Solution}}.Infrastructure/DependencyInjection.cs`
|
|
66
|
+
|
|
67
|
+
```csharp
|
|
68
|
+
using {{Solution}}.Application.Common;
|
|
69
|
+
using {{Solution}}.Infrastructure.Auth;
|
|
70
|
+
using {{Solution}}.Infrastructure.Persistence;
|
|
71
|
+
using Microsoft.EntityFrameworkCore;
|
|
72
|
+
using Microsoft.Extensions.Configuration;
|
|
73
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
74
|
+
|
|
75
|
+
namespace {{Solution}}.Infrastructure;
|
|
76
|
+
|
|
77
|
+
public static class DependencyInjection
|
|
78
|
+
{
|
|
79
|
+
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
|
|
80
|
+
{
|
|
81
|
+
var connectionString = config.GetConnectionString("Default")
|
|
82
|
+
?? throw new InvalidOperationException("ConnectionStrings:Default is required.");
|
|
83
|
+
|
|
84
|
+
services.AddDbContext<AppDbContext>(opt =>
|
|
85
|
+
opt.UseSqlServer(connectionString, sql =>
|
|
86
|
+
{
|
|
87
|
+
sql.EnableRetryOnFailure();
|
|
88
|
+
sql.CommandTimeout(60);
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
|
92
|
+
services.AddScoped<IUserContext, UserContext>();
|
|
93
|
+
|
|
94
|
+
// Auto-register concrete adapters whose interface lives in Application.
|
|
95
|
+
// Convention: Infrastructure types named *Repository implement *Repository ports.
|
|
96
|
+
services.Scan(s => s
|
|
97
|
+
.FromAssemblyOf<AssemblyMarker>()
|
|
98
|
+
.AddClasses(c => c.Where(t => t.Name.EndsWith("Repository")))
|
|
99
|
+
.AsImplementedInterfaces()
|
|
100
|
+
.WithScopedLifetime());
|
|
101
|
+
|
|
102
|
+
return services;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
internal sealed class AssemblyMarker;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## `src/{{Solution}}.Application/DependencyInjection.cs`
|
|
110
|
+
|
|
111
|
+
```csharp
|
|
112
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
113
|
+
|
|
114
|
+
namespace {{Solution}}.Application;
|
|
115
|
+
|
|
116
|
+
public static class DependencyInjection
|
|
117
|
+
{
|
|
118
|
+
public static IServiceCollection AddApplication(this IServiceCollection services)
|
|
119
|
+
{
|
|
120
|
+
services.AddAutoMapper(cfg => cfg.AddMaps(typeof(ApplicationAssemblyMarker).Assembly));
|
|
121
|
+
|
|
122
|
+
services.Scan(s => s
|
|
123
|
+
.FromAssemblyOf<ApplicationAssemblyMarker>()
|
|
124
|
+
.AddClasses(c => c.Where(t => t.Name.EndsWith("Service")))
|
|
125
|
+
.AsImplementedInterfaces()
|
|
126
|
+
.WithScopedLifetime());
|
|
127
|
+
|
|
128
|
+
return services;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public sealed class ApplicationAssemblyMarker;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## EF migrations
|
|
136
|
+
|
|
137
|
+
After scaffold, the user runs:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
dotnet ef migrations add Initial \
|
|
141
|
+
--project src/{{Solution}}.Infrastructure \
|
|
142
|
+
--startup-project src/{{Solution}}.Api
|
|
143
|
+
|
|
144
|
+
dotnet ef database update \
|
|
145
|
+
--project src/{{Solution}}.Infrastructure \
|
|
146
|
+
--startup-project src/{{Solution}}.Api
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Print this command in your final report so the user doesn't have to look it up.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Exception handler middleware
|
|
2
|
+
|
|
3
|
+
## `src/{{Solution}}.Api/Middlewares/ExceptionHandlerMiddleware.cs`
|
|
4
|
+
|
|
5
|
+
```csharp
|
|
6
|
+
using System.Net;
|
|
7
|
+
using System.Text.Json;
|
|
8
|
+
using {{Solution}}.Api.Models;
|
|
9
|
+
using {{Solution}}.Application.Common.Exceptions;
|
|
10
|
+
|
|
11
|
+
namespace {{Solution}}.Api.Middlewares;
|
|
12
|
+
|
|
13
|
+
public sealed class ExceptionHandlerMiddleware
|
|
14
|
+
{
|
|
15
|
+
private readonly RequestDelegate _next;
|
|
16
|
+
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
|
|
17
|
+
private readonly IWebHostEnvironment _env;
|
|
18
|
+
|
|
19
|
+
public ExceptionHandlerMiddleware(
|
|
20
|
+
RequestDelegate next,
|
|
21
|
+
ILogger<ExceptionHandlerMiddleware> logger,
|
|
22
|
+
IWebHostEnvironment env)
|
|
23
|
+
{
|
|
24
|
+
_next = next;
|
|
25
|
+
_logger = logger;
|
|
26
|
+
_env = env;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async Task InvokeAsync(HttpContext context)
|
|
30
|
+
{
|
|
31
|
+
try
|
|
32
|
+
{
|
|
33
|
+
await _next(context);
|
|
34
|
+
}
|
|
35
|
+
catch (Exception ex)
|
|
36
|
+
{
|
|
37
|
+
await HandleAsync(context, ex);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async Task HandleAsync(HttpContext context, Exception ex)
|
|
42
|
+
{
|
|
43
|
+
var (status, message) = ex switch
|
|
44
|
+
{
|
|
45
|
+
NotFoundException nf => ((int)HttpStatusCode.NotFound, nf.Message),
|
|
46
|
+
ValidationException v => ((int)HttpStatusCode.BadRequest, v.Message),
|
|
47
|
+
ConflictException c => ((int)HttpStatusCode.Conflict, c.Message),
|
|
48
|
+
UnauthorizedAccessException u => ((int)HttpStatusCode.Unauthorized, u.Message),
|
|
49
|
+
_ => ((int)HttpStatusCode.InternalServerError, "An unexpected error occurred.")
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (status >= 500)
|
|
53
|
+
{
|
|
54
|
+
_logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
{
|
|
58
|
+
_logger.LogWarning(ex, "Handled exception: {Message}", ex.Message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
context.Response.ContentType = "application/json";
|
|
62
|
+
context.Response.StatusCode = status;
|
|
63
|
+
|
|
64
|
+
var includeDetail = _env.IsDevelopment() || _env.IsStaging();
|
|
65
|
+
var payload = new ApiException(status, message, includeDetail ? ex.StackTrace : null);
|
|
66
|
+
|
|
67
|
+
await context.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions
|
|
68
|
+
{
|
|
69
|
+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static class MiddlewareExtensions
|
|
75
|
+
{
|
|
76
|
+
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder app) =>
|
|
77
|
+
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `src/{{Solution}}.Application/Common/Exceptions/*.cs`
|
|
82
|
+
|
|
83
|
+
```csharp
|
|
84
|
+
namespace {{Solution}}.Application.Common.Exceptions;
|
|
85
|
+
|
|
86
|
+
public class NotFoundException : Exception
|
|
87
|
+
{
|
|
88
|
+
public NotFoundException(string entity, object key)
|
|
89
|
+
: base($"{entity} with key '{key}' was not found.") { }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public class ValidationException : Exception
|
|
93
|
+
{
|
|
94
|
+
public ValidationException(string message) : base(message) { }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public class ConflictException : Exception
|
|
98
|
+
{
|
|
99
|
+
public ConflictException(string message) : base(message) { }
|
|
100
|
+
}
|
|
101
|
+
```
|