@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/bin/claude-skills.js +189 -0
  4. package/lib/install.js +111 -0
  5. package/lib/list.js +63 -0
  6. package/lib/paths.js +39 -0
  7. package/lib/remove.js +52 -0
  8. package/lib/skills.js +121 -0
  9. package/package.json +48 -0
  10. package/skills/_template/SKILL.md +34 -0
  11. package/skills/conventional-commits/SKILL.md +136 -0
  12. package/skills/diagnose/SKILL.md +140 -0
  13. package/skills/dotnet-onion-api/SKILL.md +267 -0
  14. package/skills/dotnet-onion-api/references/anti-patterns.md +155 -0
  15. package/skills/dotnet-onion-api/references/solution-layout.md +113 -0
  16. package/skills/dotnet-onion-api/references/templates/appsettings.md +75 -0
  17. package/skills/dotnet-onion-api/references/templates/base-controller.cs.md +90 -0
  18. package/skills/dotnet-onion-api/references/templates/csproj-files.md +178 -0
  19. package/skills/dotnet-onion-api/references/templates/dbcontext.cs.md +149 -0
  20. package/skills/dotnet-onion-api/references/templates/exception-middleware.cs.md +101 -0
  21. package/skills/dotnet-onion-api/references/templates/feature-slice.md +349 -0
  22. package/skills/dotnet-onion-api/references/templates/program-cs.md +171 -0
  23. package/skills/dotnet-onion-api/references/templates/worker-program.cs.md +166 -0
  24. package/skills/grill-with-docs/SKILL.md +203 -0
  25. package/skills/handoff/SKILL.md +155 -0
  26. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  27. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  28. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  29. package/skills/improve-codebase-architecture/SKILL.md +121 -0
  30. package/skills/nextjs-app-router/SKILL.md +328 -0
  31. package/skills/nextjs-app-router/references/anti-patterns.md +203 -0
  32. package/skills/nextjs-app-router/references/folder-layout.md +151 -0
  33. package/skills/nextjs-app-router/references/good-patterns.md +212 -0
  34. package/skills/nextjs-app-router/references/templates/api-base.md +62 -0
  35. package/skills/nextjs-app-router/references/templates/api-slice.md +75 -0
  36. package/skills/nextjs-app-router/references/templates/auth-slice.md +95 -0
  37. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +94 -0
  38. package/skills/nextjs-app-router/references/templates/components-json.md +41 -0
  39. package/skills/nextjs-app-router/references/templates/env-and-utils.md +110 -0
  40. package/skills/nextjs-app-router/references/templates/eslint-prettier.md +82 -0
  41. package/skills/nextjs-app-router/references/templates/feature-slice.md +184 -0
  42. package/skills/nextjs-app-router/references/templates/form-with-zod.md +192 -0
  43. package/skills/nextjs-app-router/references/templates/middleware.md +88 -0
  44. package/skills/nextjs-app-router/references/templates/next-config.md +42 -0
  45. package/skills/nextjs-app-router/references/templates/package.md +99 -0
  46. package/skills/nextjs-app-router/references/templates/providers-and-store.md +79 -0
  47. package/skills/nextjs-app-router/references/templates/root-layout.md +146 -0
  48. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +90 -0
  49. package/skills/nextjs-app-router/references/templates/tailwind-config.md +89 -0
  50. package/skills/nextjs-app-router/references/templates/testing.md +137 -0
  51. package/skills/nextjs-app-router/references/templates/tsconfig.md +40 -0
  52. package/skills/plan-and-build/SKILL.md +214 -0
  53. package/skills/pr-review/SKILL.md +132 -0
  54. 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
+ ```