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>
4.3 KiB
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/DeletedAtcolumns.
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 inBudget.Api/Models/.
- Add an
ISoftDeletableinterface (or base class) withbool IsDeleted,DateTimeOffset? DeletedAt. - Implement on
Budget,Income,Outgo,BudgetShare. - Add
byte[] RowVersion(mapped toxmin) toBudgetonly.
Phase 2 — Update AppDbContext / EF configuration
If plan #10 has been implemented,
AppDbContextis inBudget.Infrastructure/Data/.
- In
AppDbContext.OnModelCreating, for each soft-deletable entity add:builder.HasQueryFilter(x => !x.IsDeleted); - For
Budget, add the concurrency token config:builder.Property(e => e.RowVersion) .HasColumnName("xmin") .HasColumnType("xid") .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken();
Phase 3 — Update delete endpoints to soft delete
- In each controller, replace
db.X.Remove(entity)with:Affected actions:entity.IsDeleted = true; entity.DeletedAt = DateTimeOffset.UtcNow;BudgetsController.Delete,IncomesController.Delete,OutgosController.Delete,SharesController.Revoke.
Phase 4 — Handle concurrency conflicts
- In
BudgetsController.Update(and any other Budget write endpoint), wrapSaveChangesAsyncin a try/catch forDbUpdateConcurrencyExceptionand returnConflict(new { error = "The budget was modified by another user. Please refresh and retry." }).
Phase 5 — Migration
dotnet ef migrations add AddSoftDeleteAndConcurrency --project Budget.Infrastructure --startup-project Budget.Api(adjust project flags if plan #10 has not been implemented yet).- Review the generated migration — verify
xmincolumn is not added as a normal column (it should appear only inmodelSnapshot, not as anAddColumnsincexminis a Postgres system column). - Apply:
dotnet ef database updateor let startup migrations handle it.
Phase 6 — Build and verify
dotnet build— zero errors.- Manually verify that a soft-deleted budget no longer appears in
GET /api/budgetswithout 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. RowVersionis 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
Budgetis soft-deleted itsIncomes,Outgos, andBudgetSharesare not automatically soft-deleted. They are filtered out indirectly becauseBudgetsControllerchecks budget access before returning child resources. Explicit cascading can be added later.
Files affected
Budget.Core/Models/Budget.cs(orBudget.Api/Models/Budget.cs)Budget.Core/Models/Income.csBudget.Core/Models/Outgo.csBudget.Core/Models/BudgetShare.cs- New:
Budget.Core/Models/ISoftDeletable.cs(optional interface) Budget.Infrastructure/Data/AppDbContext.cs(orBudget.Api/Data/AppDbContext.cs)Budget.Api/Controllers/BudgetsController.csBudget.Api/Controllers/IncomesController.csBudget.Api/Controllers/OutgosController.csBudget.Api/Controllers/SharesController.cs- New migration file