Просмотр исходного кода

cleanup and updates

pull/1/head
Daniel Covington 1 неделю назад
Родитель
Сommit
9129688c6a
3 измененных файлов: 36 добавлений и 11 удалений
  1. +25
    -9
      .ai/skills/18-unit-of-work-pattern.md
  2. +8
    -1
      .claude/settings.local.json
  3. +3
    -1
      CLAUDE.md

+ 25
- 9
.ai/skills/18-unit-of-work-pattern.md Просмотреть файл

@@ -63,10 +63,12 @@ public sealed class UnitOfWork : IUnitOfWork

Do not create a new `DbContext` inside individual repository properties. That breaks the shared transaction/change-tracking boundary.

### 2. Register Unit of Work as scoped in ASP.NET Core apps
### 2. Register Unit of Work as scoped; use IServiceScopeFactory for per-operation work in desktop apps

EF Core contexts are normally scoped per request. Register the Unit of Work with the same lifetime.

**ASP.NET Core (per-request scope managed by the framework):**

```csharp
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
@@ -74,6 +76,24 @@ builder.Services.AddDbContext<AppDbContext>(options =>
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
```

**WinForms / WPF / console (per-operation scope managed manually):**

```csharp
services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));
services.AddScoped<IUnitOfWork, UnitOfWork>();

// In a form or service, inject IServiceScopeFactory and create one scope per operation:
private async Task SaveAsync()
{
using var scope = _scopeFactory.CreateScope();
var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
await uow.Orders.AddAsync(order);
await uow.SaveChangesAsync();
}
```

This keeps one fresh `DbContext` per logical operation and avoids stale change-tracking across user actions.

Avoid `Singleton` for `DbContext`, repositories, or Unit of Work. Prefer `Scoped` for web apps. `Transient` can work in simple cases, but `Scoped` is the safer default when the Unit of Work wraps a scoped `DbContext`.

### 3. Repositories stage changes; Unit of Work saves changes
@@ -171,7 +191,7 @@ public interface IGenericRepository<TEntity> where TEntity : class
### Unit of Work

```csharp
public interface IUnitOfWork : IAsyncDisposable
public interface IUnitOfWork
{
IGenericRepository<Customer> Customers { get; }
IGenericRepository<Order> Orders { get; }
@@ -180,7 +200,7 @@ public interface IUnitOfWork : IAsyncDisposable
}
```

Use `IAsyncDisposable` when the Unit of Work owns the context disposal. In ASP.NET Core dependency injection, the container usually disposes the scoped context. In that case, implementing disposal is optional unless the project convention requires it.
Do not extend `IUnitOfWork` with `IAsyncDisposable` unless the Unit of Work explicitly owns the context and you also implement `IDisposable`. If `IUnitOfWork` only implements `IAsyncDisposable`, the DI container will throw `InvalidOperationException` when it tries to synchronously dispose a scope — which happens in WinForms, WPF, console apps, and unit tests. In ASP.NET Core, the container disposes the scoped `DbContext` automatically; the Unit of Work does not need to manage context disposal at all.

## Recommended Implementation

@@ -303,15 +323,10 @@ public sealed class UnitOfWork : IUnitOfWork
{
return _context.SaveChangesAsync(cancellationToken);
}

public ValueTask DisposeAsync()
{
return _context.DisposeAsync();
}
}
```

If ASP.NET Core dependency injection owns the `DbContext`, do not manually dispose it inside request code. Let the container manage scoped disposal.
The DI container disposes the scoped `DbContext` when the scope ends. Do not add a `DisposeAsync` or `Dispose` method to `UnitOfWork` unless the project explicitly creates and owns the context outside of DI. If you do add disposal, implement **both** `IDisposable` and `IAsyncDisposable` — implementing only `IAsyncDisposable` causes the container to throw when disposing a scope synchronously (for example in WinForms, WPF, console, or test code that uses `using var scope`).

## Query Rules

@@ -426,6 +441,7 @@ For Unit of Work code:
- Using string includes when typed includes are available.
- Ignoring cancellation tokens.
- Treating Unit of Work as a substitute for business/domain services.
- Implementing only `IAsyncDisposable` on `UnitOfWork` — the DI container throws `InvalidOperationException` when it synchronously disposes a scope (WinForms, WPF, console, tests). Either implement both `IDisposable` and `IAsyncDisposable`, or implement neither and let the container own `DbContext` disposal.

## Adding a New Entity Repository



+ 8
- 1
.claude/settings.local.json Просмотреть файл

@@ -4,7 +4,14 @@
"Bash(Get-ChildItem -Path \"C:\\\\Users\\\\Admin\\\\Downloads\\\\csharp-dotnet-ai-agent-pack-generic-uow\\\\csharp-dotnet-ai-agent-pack\" -Recurse -File)",
"Bash(Select-Object FullName)",
"Bash(Sort-Object FullName)",
"PowerShell($path = \"C:\\\\Users\\\\Admin\\\\Downloads\\\\csharp-dotnet-ai-agent-pack-generic-uow\\\\csharp-dotnet-ai-agent-pack\"; if \\(Test-Path $path\\) { Write-Host \"Path exists\"; Get-ChildItem $path -Recurse | Sort-Object FullName | ForEach-Object { $_.FullName } } else { Write-Host \"Path does not exist\"; Get-ChildItem \"C:\\\\Users\\\\Admin\\\\Downloads\\\\\" })"
"PowerShell($path = \"C:\\\\Users\\\\Admin\\\\Downloads\\\\csharp-dotnet-ai-agent-pack-generic-uow\\\\csharp-dotnet-ai-agent-pack\"; if \\(Test-Path $path\\) { Write-Host \"Path exists\"; Get-ChildItem $path -Recurse | Sort-Object FullName | ForEach-Object { $_.FullName } } else { Write-Host \"Path does not exist\"; Get-ChildItem \"C:\\\\Users\\\\Admin\\\\Downloads\\\\\" })",
"Bash(dotnet new *)",
"Bash(dotnet sln *)",
"Bash(dotnet restore *)",
"Bash(dotnet build *)"
],
"additionalDirectories": [
"C:\\Users\\Admin\\Downloads\\EmployeeManager\\EmployeeManager"
]
}
}

+ 3
- 1
CLAUDE.md Просмотреть файл

@@ -1 +1,3 @@
AGENTS.m
# Project Instructions

See [AGENTS.md](AGENTS.md) for all agent operating rules, skill routing, engineering standards, and workflow conventions that apply to this repository.

Загрузка…
Отмена
Сохранить

Powered by TurnKey Linux.