087fbdd176
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps) Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService Budget.Api: controllers, middleware, Program.cs — references both projects EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer, HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated to copy all three project directories before publishing. Migration namespaces updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model type strings updated to Budget.Core.Models.* in the snapshot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
4.3 KiB
Markdown
107 lines
4.3 KiB
Markdown
# Plan: Soft Delete and Concurrency Tokens
|
|
|
|
## Goal
|
|
|
|
Add soft delete (`IsDeleted` / `DeletedAt` + global EF query filter) and a `xmin`-based
|
|
concurrency token (`RowVersion`) to the entities that benefit from them, matching the
|
|
stwaddle stack convention.
|
|
|
|
## Current state
|
|
|
|
- Hard deletes on all entities — `db.X.Remove(entity); await db.SaveChangesAsync()`.
|
|
- No concurrency tokens — concurrent updates overwrite each other silently.
|
|
- No `IsDeleted` / `DeletedAt` columns.
|
|
|
|
## Scope decisions
|
|
|
|
**Soft delete:** Apply to `Budget`, `Income`, `Outgo`, and `BudgetShare`.
|
|
`KnownUser` is a provisioning-only table; hard deletes are fine there.
|
|
|
|
**Concurrency token:** Apply to `Budget` only for now — it is the entity most
|
|
likely to be edited by multiple users simultaneously (owner + shared editor).
|
|
`Income` and `Outgo` updates are user-local; `BudgetShare` and `KnownUser` are
|
|
low-contention. Extend later if needed.
|
|
|
|
## Steps
|
|
|
|
### Phase 1 — Update entity models
|
|
|
|
> If plan #10 (project split) has been implemented, these files live in `Budget.Core/Models/`.
|
|
> Otherwise they are in `Budget.Api/Models/`.
|
|
|
|
1. Add an `ISoftDeletable` interface (or base class) with `bool IsDeleted`, `DateTimeOffset? DeletedAt`.
|
|
2. Implement on `Budget`, `Income`, `Outgo`, `BudgetShare`.
|
|
3. Add `byte[] RowVersion` (mapped to `xmin`) to `Budget` only.
|
|
|
|
### Phase 2 — Update AppDbContext / EF configuration
|
|
|
|
> If plan #10 has been implemented, `AppDbContext` is in `Budget.Infrastructure/Data/`.
|
|
|
|
4. In `AppDbContext.OnModelCreating`, for each soft-deletable entity add:
|
|
```csharp
|
|
builder.HasQueryFilter(x => !x.IsDeleted);
|
|
```
|
|
5. For `Budget`, add the concurrency token config:
|
|
```csharp
|
|
builder.Property(e => e.RowVersion)
|
|
.HasColumnName("xmin")
|
|
.HasColumnType("xid")
|
|
.ValueGeneratedOnAddOrUpdate()
|
|
.IsConcurrencyToken();
|
|
```
|
|
|
|
### Phase 3 — Update delete endpoints to soft delete
|
|
|
|
6. In each controller, replace `db.X.Remove(entity)` with:
|
|
```csharp
|
|
entity.IsDeleted = true;
|
|
entity.DeletedAt = DateTimeOffset.UtcNow;
|
|
```
|
|
Affected actions: `BudgetsController.Delete`, `IncomesController.Delete`,
|
|
`OutgosController.Delete`, `SharesController.Revoke`.
|
|
|
|
### Phase 4 — Handle concurrency conflicts
|
|
|
|
7. In `BudgetsController.Update` (and any other Budget write endpoint), wrap
|
|
`SaveChangesAsync` in a try/catch for `DbUpdateConcurrencyException` and return
|
|
`Conflict(new { error = "The budget was modified by another user. Please refresh and retry." })`.
|
|
|
|
### Phase 5 — Migration
|
|
|
|
8. `dotnet ef migrations add AddSoftDeleteAndConcurrency --project Budget.Infrastructure --startup-project Budget.Api`
|
|
(adjust project flags if plan #10 has not been implemented yet).
|
|
9. Review the generated migration — verify `xmin` column is not added as a normal column
|
|
(it should appear only in `modelSnapshot`, not as an `AddColumn` since `xmin` is a Postgres
|
|
system column).
|
|
10. Apply: `dotnet ef database update` or let startup migrations handle it.
|
|
|
|
### Phase 6 — Build and verify
|
|
|
|
11. `dotnet build` — zero errors.
|
|
12. Manually verify that a soft-deleted budget no longer appears in `GET /api/budgets`
|
|
without any controller changes (query filter handles it).
|
|
|
|
## Key decisions
|
|
|
|
- No admin endpoint to view/restore deleted records is in scope here — add `IgnoreQueryFilters()`
|
|
to a future admin endpoint if needed.
|
|
- `RowVersion` is not exposed in DTOs or returned to the client in this plan.
|
|
If optimistic concurrency is needed client-side (e.g., ETag), that is a separate concern.
|
|
- Cascade behavior: when a `Budget` is soft-deleted its `Incomes`, `Outgos`, and `BudgetShares`
|
|
are not automatically soft-deleted. They are filtered out indirectly because `BudgetsController`
|
|
checks budget access before returning child resources. Explicit cascading can be added later.
|
|
|
|
## Files affected
|
|
|
|
- `Budget.Core/Models/Budget.cs` (or `Budget.Api/Models/Budget.cs`)
|
|
- `Budget.Core/Models/Income.cs`
|
|
- `Budget.Core/Models/Outgo.cs`
|
|
- `Budget.Core/Models/BudgetShare.cs`
|
|
- New: `Budget.Core/Models/ISoftDeletable.cs` (optional interface)
|
|
- `Budget.Infrastructure/Data/AppDbContext.cs` (or `Budget.Api/Data/AppDbContext.cs`)
|
|
- `Budget.Api/Controllers/BudgetsController.cs`
|
|
- `Budget.Api/Controllers/IncomesController.cs`
|
|
- `Budget.Api/Controllers/OutgosController.cs`
|
|
- `Budget.Api/Controllers/SharesController.cs`
|
|
- New migration file
|