This is the root-level guide for the entire DevCoreApp solution. Project-specific conventions (page patterns, service patterns, mocks) are in src/Server/Admin/WebService/CLAUDE.md.
DevCoreApp is a reusable starter template for custom ERP and CRM applications. It provides user management, permissions, organization hierarchy, background jobs, email, notifications, and file storage out of the box. Each new project forks this template and builds domain features on top.
- .NET 10+, ASP.NET Core, Entity Framework Core, ASP.NET Identity
- Blazor SSR (Admin UI), Blazor WebAssembly (field worker client)
- PostgreSQL (primary), SQL Server (secondary)
- DevInstance.BlazorToolkit — client-side Blazor utilities (
[BlazorService],IApiContext<T>,IServiceExecutionHost) - DevInstance.WebServiceToolkit — server-side utilities (
[WebService],[QueryModel],ModelItem,ModelList<T>,HandleWebRequestAsync(),IModelQuery<T,D>) - DevInstance.LogScope — scope-based logging (
IScopeManager,IScopeLog)
DevCoreApp/
├── src/
│ ├── Client/
│ │ ├── DevCoreApp.Client/ # Blazor WASM app
│ │ └── DevCoreApp.Client.Services/ # Client-side API clients
│ ├── Server/
│ │ ├── Admin/
│ │ │ ├── DevCoreApp.Admin.Services/ # Business logic, auth, notifications
│ │ │ └── DevCoreApp.Admin.WebService/ # Blazor SSR host + API controllers + SignalR
│ │ ├── DevCoreApp.Worker/ # Background job worker (separate host)
│ │ ├── DevCoreApp.Database/ # Entities, queries, decorators, EF config
│ │ ├── DevCoreApp.Email/ # Email provider implementations
│ │ └── DevCoreApp.Storage/ # File storage provider implementations
│ └── Shared/ # ViewModels, constants, enums
├── mocks/
│ └── Server/Admin/ServicesMocks/ # Mock services for UI development
├── tests/ # Mirrors src/ structure
└── docs/
DevCoreApp.Shared ← Referenced by everything. No project dependencies.
DevCoreApp.Database ← References: Shared
DevCoreApp.Email ← References: Shared
DevCoreApp.Storage ← References: Shared
DevCoreApp.Admin.Services ← References: Database, Email, Storage, Shared
DevCoreApp.Admin.WebService ← References: Admin.Services, Shared
DevCoreApp.Worker ← References: Admin.Services, Database, Shared
DevCoreApp.Client.Services ← References: Shared
DevCoreApp.Client ← References: Client.Services, Shared
Hard rules:
- Client and Client.Services NEVER reference Database or any Server project
- Database NEVER references Admin.Services, Admin.WebService, or ASP.NET Core HTTP abstractions
- Admin.WebService NEVER references Database directly — always through Admin.Services
- Worker is a separate hosted process from WebService. Do not merge them.
- PascalCase everywhere — tables, columns, classes, properties, DTOs. No underscores.
- ViewModels:
{Entity}Item(e.g.,UserProfileItem,InvoiceItem) - Service interface:
I{Entity}Service/ Implementation:{Entity}Service/ Mock:{Entity}ServiceMock - Decorators:
{Entity}Decorators— extension methodsToView()(entity → ViewModel) andToRecord()(ViewModel → entity) - Query classes:
{Entity}QueryorCore{Entity}Query - Permissions:
Module.Entity.Actionformat (e.g.,Sales.Invoice.Approve) - ASP.NET Identity tables keep default names (
AspNetUsers,AspNetRoles, etc.)
All entities inherit from one of three base classes in Database/Core/Models/Base/:
DatabaseBaseObject
├── Id (Guid) — internal PK, never exposed to client
DatabaseObject : DatabaseBaseObject
├── PublicId (string) — client-facing ID, generated via IdGenerator.New()
├── CreateDate (DateTime)
└── UpdateDate (DateTime)
DatabaseEntityObject : DatabaseObject
├── CreatedBy (→ UserProfile) — navigation property
└── UpdatedBy (→ UserProfile) — navigation property
- Use
DatabaseBaseObjectfor infrastructure tables (AuditLogs, JobLogs, Settings) - Use
DatabaseObjectfor entities exposed via API but without user tracking - Use
DatabaseEntityObjectfor business entities that track who created/modified them
The Id (Guid) never leaves the server. APIs use PublicId. Decorators map PublicId → ModelItem.Id on ViewModels.
Services NEVER call DbContext directly. All data access goes through query classes.
Service
→ Repository.Get{Entity}Query(AuthorizationContext.CurrentProfile)
→ returns query class implementing IModelQuery<T,D>
→ supports .Top(), .Page(), .Search(), .Sort() via IQPageable, IQSearchable, IQSortable
Decorators convert between entities and ViewModels. They are extension methods, not services:
entity.ToView()→ returns{Entity}ItemViewModelentity.ToRecord(dto)→ maps DTO fields onto entity
Cross-feature queries live in the feature that owns the primary entity. An invoice report query lives in Database/Invoices/, not in a separate Reporting/ folder.
Data is scoped by Organization, not by Tenant. Tenant is a thin deployment-level record (one per database — license, plan, subdomain). Organization is a hierarchical tree for data isolation.
Tenant: "Acme Corp"
└── Root Org: Acme Corp
├── East Region
│ ├── New York Office
│ └── Boston Office
└── West Region
All business tables have OrganizationId. EF Core global query filter automatically restricts queries to the user's visible organizations.
Users connect to organizations via UserOrganizations:
Scope = Self→ sees only that organization's dataScope = WithChildren→ sees that organization + all descendants
IOperationContext provides the resolved context to the data layer:
UserId,PrimaryOrganizationId,VisibleOrganizationIds,IpAddress,CorrelationId- Populated from HTTP context in WebService, from job context in Worker
- Database project depends on this interface, NOT on
IHttpContextAccessor
When creating new records, set OrganizationId to IOperationContext.PrimaryOrganizationId.
ASP.NET Identity handles roles. DevCoreApp adds a permission layer on top via claims transformation.
Flow:
- User logs in → Identity loads roles from
AspNetUserRoles IClaimsTransformationresolves role → permission mappings fromRolePermissionstable- Checks
UserPermissionOverridesfor per-user grants/denials - Injects
Permission:Module.Entity.Actionclaims intoClaimsPrincipal [Authorize(Policy = "Sales.Invoice.Approve")]checks for the claim
Permission keys use Module.Entity.Action format. Define them as constants in PermissionDefinitions:
public static class Sales
{
public static class Invoice
{
public const string View = "Sales.Invoice.View";
public const string Approve = "Sales.Invoice.Approve";
}
}Use permissions, not roles, for authorization checks. [Authorize(Roles = "Admin")] is acceptable for broad checks, but feature-level access must use [Authorize(Policy = "...")].
Dual mechanism:
- EF Core
SaveChangesInterceptor— catches all changes through the application. Has full user context. This is the primary mechanism. - Database triggers — on critical tables only (financial records, user credentials, permission tables). Catches changes from any source. No user context available.
Both write to the same AuditLogs table with a Source column (Application vs Database).
Sensitive fields decorated with [AuditExclude] are omitted from audit values (e.g., PasswordHash, SecurityStamp).
Jobs are persisted to the Jobs table BEFORE the worker picks them up. This prevents job loss on process restart.
Flow: Create Job record → Worker picks up → Creates JobLogs entry per attempt → Updates Job status
ResultReference links a job to its domain entity (e.g., EmailLog:abc-123). Domain tables own business state; Jobs owns execution state.
The Worker (DevCoreApp.Worker) runs as a separate process. It can be deployed and restarted independently from WebService.
Provider-based file storage with local disk (default) and S3 (stub). Configuration and usage details: src/Server/Storage/FileStorage.md.
Quick reference: Files are uploaded via IFileService.UploadAsync(), metadata stored in FileRecords table (organization-scoped), physical files stored by IFileStorageProvider. Provider is registered in Program.cs via AddLocalFileStorage(). Runtime limits (max size, allowed types, soft-delete) are managed via the Settings table under the Storage category.
- Controllers use
HandleWebRequestAsync()from WebServiceToolkit - Use WebServiceToolkit exception types:
BadRequestException(400),UnauthorizedException(401),RecordNotFoundException(404),RecordConflictException(409) - Use
BusinessRuleExceptionfor domain validation failures (422) - Do NOT throw generic
ExceptionorInvalidOperationExceptionfor expected error cases
Each project uses vertical slices — group by feature, not by technical layer:
Database/
├── Invoices/
│ ├── Invoice.cs # Entity
│ ├── InvoiceConfiguration.cs # EF config
│ ├── InvoiceQuery.cs # Query implementation
│ └── InvoiceDecorator.cs # Entity ↔ ViewModel mapper
Admin.Services/
├── Invoices/
│ ├── InvoiceService.cs # Business logic
│ └── InvoiceValidator.cs # Validation rules
Admin.WebService/
├── Invoices/
│ ├── InvoiceController.cs # API endpoints
│ ├── InvoiceListPage.razor # Admin UI
│ └── InvoiceDetailPage.razor
Shared/
├── Invoices/
│ ├── InvoiceItem.cs # ViewModel
│ └── InvoiceCreateRequest.cs # Request DTO
- Run
dotnet buildbefore committing to verify compilation - Use
IdGenerator.New()for PublicId values, neverGuid.NewGuid().ToString() - Use
query.CreateNew()to instantiate entities — nevernew Entity { ... }directly. The query'sCreateNew()method setsId,PublicId,CreateDate,UpdateDate(and other base fields) consistently viaIdGeneratorandITimeProvider. The only exception is data seeders that run during database initialization. - Use LogScope (
IScopeLog) for logging, notILogger - Use
[AuditExclude]on sensitive entity properties - Set
OrganizationIdon new business records - Return
ServiceActionResult<T>from services, not raw values or exceptions - Use
ModelList<T>for paginated responses
- Never expose
Id(Guid PK) to the client — usePublicId - Never call
DbContextdirectly from a service — use query classes - Never instantiate entities directly with
new Entity { ... }— usequery.CreateNew()instead (seeders are the only exception) - Never inject
DbContextor database types into pages or controllers - Never add ASP.NET Core HTTP dependencies to the Database project
- Never put business logic in entity classes — keep them as data models
- Never create
InputModelclasses in pages — DTOs carry validation attributes - Never bypass the organization scoping filter with
IgnoreQueryFilters()unless explicitly required for admin/system operations - Never use
ILogger/LogInformation— useIScopeLogfrom DevInstance.LogScope - Never create or scaffold EF Core migrations (
dotnet ef migrations add) — notify the user that a migration is needed and let them create it