Apply base entity inheritance consistently across all modules#102
Merged
antosubash merged 5 commits intomainfrom Apr 11, 2026
Merged
Apply base entity inheritance consistently across all modules#102antosubash merged 5 commits intomainfrom
antosubash merged 5 commits intomainfrom
Conversation
Replace ad-hoc timestamp/audit fields with inheritance from the Core base entities (Entity<T>, AuditableEntity<T>, FullAuditableEntity<T>). This brings automatic CreatedAt/UpdatedAt tracking, concurrency stamps, versioning, and soft-delete semantics to modules that were previously rolling their own or missing them entirely. Changes: - BackgroundJobs: JobProgress and JobQueueEntryEntity now extend Entity<Guid>. - Settings: SettingEntity and PublicMenuItemEntity now extend Entity<int>; services drop manual UpdatedAt assignments. - Chat: Conversation extends Entity<ConversationId>, ChatMessage extends Entity<ChatMessageId>; UpdatedAt is maintained on append/rename. - FileStorage: StoredFile extends Entity<FileStorageId>. - Email: EmailMessage and EmailTemplate extend Entity<TId> / AuditableEntity<TId>; DateTime fields migrated to DateTimeOffset and query filters updated accordingly. - Orders: Order extends AuditableEntity<OrderId> with DateTimeOffset timestamps; seed service updated to emit DateTimeOffset values. - PageBuilder: Page uses FullAuditableEntity<PageId> (soft delete via IsDeleted/DeletedAt), PageTemplate uses Entity<PageTemplateId>, PageSummary DTO switched to DateTimeOffset; permanent-delete path uses ExecuteDeleteAsync to bypass the soft-delete interceptor. - Products: Product extends Entity<ProductId> with deterministic seed timestamps so EF Core migrations remain stable. - RateLimiting: RateLimitRule extends Entity<RateLimitRuleId>. - Agents: AgentSession gains inherited audit fields via Entity<string>. SQLite support: modules that now carry DateTimeOffset properties and run queries against SQLite (tests, local dev) apply DateTimeOffsetToBinaryConverter conditionally so ORDER BY and range filters translate. A new DatabaseOptionsExtensions.DetectProvider() helper centralizes the provider detection for this decision, falling back gracefully when only a module-specific connection string is configured. A single EF Core migration (UseBaseEntities) adds the new columns (ConcurrencyStamp, CreatedAt/UpdatedAt, soft-delete fields, version) and adjusts the existing DateTime columns where modules moved to DateTimeOffset.
Every remaining entity that still used plain int/Guid/string IDs now uses a Vogen value object, matching the rest of the codebase. This gives the entity layer the same type safety the contracts layer already has and removes the last few mismatched ID types across modules. New Vogen IDs (added to each module's Contracts project): - Settings.Contracts: SettingId, PublicMenuItemId (int-backed) - FeatureFlags.Contracts: FeatureFlagId, FeatureFlagOverrideId (int-backed) - Agents.Contracts: AgentSessionId, AgentMessageId (string-backed) Reused existing Vogen IDs: - BackgroundJobs.Contracts.JobId for both JobProgress and JobQueueEntryEntity (they share the logical job identifier) Entity/service changes: - Entities switched to Entity<TId> with the new strongly-typed IDs. - DbContexts register EfCoreValueConverter/Comparer conventions for each new ID type. - Services and endpoints accept/return the typed IDs, with the natural conversion boundaries (route-bound int -> Vogen.From(int), JobQueueEntry DTO Guid <-> JobId.From(guid)) happening where infrastructure types cross. - Internal stores (InMemoryAgentSessionStore, EfAgentSessionStore) key their dictionaries/queries on AgentSessionId while keeping the public IAgentSessionStore string-based API unchanged. Contracts DTO updates: - FeatureFlagOverride.Id, PublicMenuItemDto.Id/ParentId, CreateMenuItemRequest.ParentId, ReorderItem.Id/ParentId now carry the typed IDs end to end. - IFeatureFlagContracts.DeleteOverrideAsync takes FeatureFlagOverrideId; endpoint converts the route int. Unit tests updated for the new ID types (FindAsync calls, seeded entity initializers, service-level invocations). A fresh EF Core migration replaces the previous UseBaseEntities migration and adds no schema changes (Vogen converters preserve the underlying column types) but updates the model snapshot to record the new value-converter wiring.
Moves every EF Core entity that was still living in an implementation project into its module's `.Contracts` assembly, and adds a Roslyn-powered compile-time diagnostic (SM0055) so the rule holds going forward. Entities relocated: - BackgroundJobs: JobProgress, JobQueueEntryEntity - Settings: SettingEntity, PublicMenuItemEntity - FeatureFlags: FeatureFlagEntity, FeatureFlagOverrideEntity - Agents: AgentSession, AgentMessage - Datasets: Dataset - Tenants: TenantEntity, TenantHostEntity - Permissions: RolePermission, UserPermission (junction tables) Each moved entity is tagged with `[NoDtoGeneration]` where it isn't a true DTO, so the TypeScript generator still only emits types the frontend actually consumes. `IEntityTypeConfiguration<T>` mappings, the `DbContext` itself, services, and endpoints stay where they were — only the entity class crosses the assembly boundary. Supporting changes: - Permissions.Contracts now references EntityFrameworkCore (needed so the relocated junction types can sit next to the existing contract interfaces). - FeatureFlags entity configurations moved from the old `Entities/` folder to a dedicated `EntityConfigurations/` folder, matching the convention used by the other modules. - Settings/Agents/Permissions/Tenants/Datasets services updated to import from `*.Contracts` instead of the deleted `*.Entities` namespaces. New diagnostic SM0055: - Fires on every `DbSet<T>` whose entity type is source-declared in a `SimpleModule.*` assembly that isn't a `.Contracts` assembly. - Skips Identity/OpenIddict entities (external metadata-only types the author can't move). - Suggests the correct replacement assembly, including the special case where the implementation is suffixed with `.Module` (SimpleModule.Agents.Module → SimpleModule.Agents.Contracts). - Registered in `AnalyzerReleases.Unshipped.md`. Generator plumbing: - `DbSetInfoRecord` now carries the entity's containing assembly name and source location so the diagnostic can pinpoint the offending type. - Discovery walks the `DbSet<T>` type argument and records those fields alongside the existing property name + FQN. Tests: - Four new generator tests cover SM0055: offending implementation assembly, compliant contracts assembly, the `.Module`-suffixed sibling case, and non-`SimpleModule.*` external assemblies. - `GeneratorTestHelper` gains `CreateEfCoreCompilationWithAssemblyName()` so tests can simulate the assembly name the diagnostic inspects. - All 193 Generator.Tests pass; affected module test suites (Settings, FeatureFlags, BackgroundJobs, Tenants, Permissions, Datasets) pass cleanly. Constitution: - Section 2 "What a Module Must Always Expose" now lists entity classes as required Contracts content (with the `[NoDtoGeneration]` guidance). - Section 4 "Data Ownership" inverted: entities live in Contracts; configurations, DbContext, and services stay in implementation. - Compiler-enforced rules table updated with SM0055.
Code-review pass on the SM0055 commit turned up a few small wins: - Introduce `AssemblyConventions` (FrameworkPrefix, ContractsSuffix, ModuleSuffix, GetExpectedContractsAssemblyName). The SM0055 emitter was hardcoding `"SimpleModule."`, `".Contracts"`, and `".Module"` as literals and open-coding the `.Module` → `.Contracts` suffix swap. Other diagnostics in the same file also reach for the same literals, so having a named home for them makes future renames trivial. - Fuse the SM0055 and SM0006 iterations over `data.DbContexts` / `DbSets`. They previously walked the exact same nested loop back-to-back; now the first pass builds `allEntityFqns` as a side effect and the SM0006 loop only consumes it. - Drop the redundant `string.IsNullOrEmpty(EntityAssemblyName)` guard in SM0055 — `EntityLocation is null` already covers the metadata-only / missing-symbol case, and the guards now read as one coherent "skip anything we can't flag" block. - Delete the `CreateCompilationWithAssemblyName` / `CreateEfCoreCompilationWithAssemblyName` test helpers. The unparameterised variant was never called, and the EfCore variant was a one-line `.WithAssemblyName(...)` wrapper. Inline the call at the four SM0055 test sites so the Roslyn API is visible and the test helper's surface stops growing. - Document `TenantEntity.ConnectionString` as infrastructure-only. The field is now visible through `Tenants.Contracts`, so flag the intent in a comment — other modules must still go through `ITenantsContracts`, never read the raw string. All 193 generator tests continue to pass. Full solution build is clean.
The CI build job failed on
`DatabaseJobQueueTests.DequeueAsync_ReturnsAndClaimsOldestPending`
with `System.InvalidCastException: Object must implement IConvertible`.
The test does:
var row = await _db.JobQueueEntries.SingleAsync(e => e.Id == claimed.Id);
After the Vogen-ID migration, `e.Id` is `JobId` (Vogen struct wrapping
Guid) but `claimed.Id` is still a plain `Guid` on the `JobQueueEntry`
transport record. When EF Core builds the query parameter it runs the
`JobId.EfCoreValueConverter` sanitiser on the raw `Guid`, which ends
up calling `Convert.ChangeType(guid, typeof(JobId))` — and Vogen
value objects don't implement `IConvertible`, so the call throws.
Fix: wrap `claimed.Id` in `JobId.From(...)` at the test site so both
sides of the comparison are already `JobId`. EF Core's SQL
translator then converts the typed parameter through the normal
`JobId → Guid` path without touching the sanitiser.
All 97 BackgroundJobs.Tests pass locally with the fix.
Deploying simplemodule-website with
|
| Latest commit: |
096dd75
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://90342b19.simplemodule-website.pages.dev |
| Branch Preview URL: | https://claude-fix-base-entity-usage.simplemodule-website.pages.dev |
antosubash
pushed a commit
that referenced
this pull request
Apr 11, 2026
PR #102 moved the Dataset entity out of SimpleModule.Datasets.Entities and into SimpleModule.Datasets.Contracts alongside the other module entities. The merge into this branch broke CS0234 on the now-missing namespace import in my test file; drop the using and rely on the existing SimpleModule.Datasets.Contracts import for the Dataset type.
antosubash
added a commit
that referenced
this pull request
Apr 11, 2026
* Purge dataset storage blobs in background job on delete
DeleteAsync previously soft-deleted the row but never removed the
original upload, normalized GeoJSON cache, or any derivative blobs —
so every deleted dataset leaked its entire storage footprint (often
GBs). Add PurgeDatasetJob that runs after the soft-delete and walks
the metadata to delete every referenced blob off the critical path.
* Install native GIS tooling in runtime image for Datasets module
The Datasets module's background jobs shell out to native GIS tools:
gdal_translate/gdalwarp/ogr2ogr for rasters + non-GeoJSON vector
formats, SpatiaLite for GeoPackage reads, and tippecanoe for
vector→PMTiles. None of these ship with the dotnet/aspnet base image.
- Runtime stage now installs gdal-bin, libsqlite3-mod-spatialite, unzip
- New tippecanoe-builder stage compiles felt/tippecanoe from source
(not in Debian repos) and the runtime copies the 6 installed binaries
from /opt/tippecanoe/bin/
Adds ~300 MB to the runtime image (GDAL pulls libproj/libgeos/libnetcdf)
but is unavoidable for real GIS support.
* Move GIS tooling from Host Dockerfile to new Worker Dockerfile
Split concerns so only the process that actually runs IModuleJob handlers
carries the ~300 MB of native GIS dependencies. The Host runs in
BackgroundJobs:WorkerMode=Producer and only enqueues jobs, so gdal-bin,
SpatiaLite, and tippecanoe have no business being on its image.
- Dockerfile: reverted to its original state (no GIS apt installs, no
tippecanoe-builder stage)
- Dockerfile.worker: new, builds template/SimpleModule.Worker with its
own tippecanoe-builder stage, gdal-bin / libsqlite3-mod-spatialite /
unzip in the runtime, and dotnet/runtime:10.0 base (no ASP.NET stack
needed since the Worker is a Generic Host). Installs Node in the
build stage because the ExtractDtoTypeScript / ExtractRoutes MSBuild
targets in SimpleModule.Hosting.targets shell out to `node tools/*.mjs`
after CoreCompile.
- SimpleModule.Worker.csproj: add reference to SimpleModule.Datasets so
the source generator registers ProcessDatasetJob / ConvertDatasetJob /
PurgeDatasetJob for the consumer to execute.
- docker-compose.yml: add worker service built from Dockerfile.worker,
pin api service to WorkerMode=Producer for explicitness, introduce
a shared storage_data named volume so uploaded dataset blobs written
by the Host are visible to the Worker when the background job picks
them up.
Validated Worker publish against a git-archive-clean source tree
(mirroring the Docker build context with no node_modules, no wwwroot) —
builds clean with only the expected "node_modules not found" warnings
and produces SimpleModule.{Worker,Datasets,BackgroundJobs,Email}.dll.
* Simplify PurgeDatasetJob and trim verbose docker comments
- PurgeDatasetJob: add .AsNoTracking() on the row load (read-only), drop
the duplicate DeserializeMetadata helper in favour of inline
JsonSerializer.Deserialize, collect blob paths up front and parallelize
deletes with Task.WhenAll instead of awaiting each one sequentially.
Removes the mutable deleted counter in the process.
- DatasetsContractsService.DeleteAsync: drop the narrative comment above
the EnqueueAsync call — the PurgeDatasetJob summary already explains why.
- Dockerfile.worker: drop per-stage banner comments that just restate the
FROM ... AS label. Keep the top-of-file block and the tippecanoe
source-build rationale.
- docker-compose.yml: drop the apologetic "set here for visibility"
comment on the worker's BackgroundJobs__WorkerMode env var.
* Drop stale Datasets.Entities import in PurgeDatasetJobTests
PR #102 moved the Dataset entity out of SimpleModule.Datasets.Entities
and into SimpleModule.Datasets.Contracts alongside the other module
entities. The merge into this branch broke CS0234 on the now-missing
namespace import in my test file; drop the using and rely on the
existing SimpleModule.Datasets.Contracts import for the Dataset type.
---------
Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace ad-hoc timestamp/audit fields with inheritance from the Core
base entities (Entity, AuditableEntity, FullAuditableEntity).
This brings automatic CreatedAt/UpdatedAt tracking, concurrency stamps,
versioning, and soft-delete semantics to modules that were previously
rolling their own or missing them entirely.
Changes:
Entity.
Entity; services drop manual UpdatedAt assignments.
extends Entity; UpdatedAt is maintained on
append/rename.
AuditableEntity; DateTime fields migrated to DateTimeOffset
and query filters updated accordingly.
timestamps; seed service updated to emit DateTimeOffset values.
IsDeleted/DeletedAt), PageTemplate uses Entity,
PageSummary DTO switched to DateTimeOffset; permanent-delete path
uses ExecuteDeleteAsync to bypass the soft-delete interceptor.
seed timestamps so EF Core migrations remain stable.
SQLite support: modules that now carry DateTimeOffset properties and
run queries against SQLite (tests, local dev) apply
DateTimeOffsetToBinaryConverter conditionally so ORDER BY and range
filters translate. A new DatabaseOptionsExtensions.DetectProvider()
helper centralizes the provider detection for this decision, falling
back gracefully when only a module-specific connection string is
configured.
A single EF Core migration (UseBaseEntities) adds the new columns
(ConcurrencyStamp, CreatedAt/UpdatedAt, soft-delete fields, version)
and adjusts the existing DateTime columns where modules moved to
DateTimeOffset.