Pages must never access the database directly. All operations go through services via IServiceExecutionHost.
Page (Blazor) → Host.ServiceReadAsync / Host.ServiceSubmitAsync → Service → Repository
- Inject the service via interface:
[Inject] private IUserProfileService UserService { get; set; } = default!; - Receive the host as cascading parameter:
[CascadingParameter] private IServiceExecutionHost Host { get; set; } = default!; - Use
Host.ServiceReadAsync(async () => await Service.Method(), result => Property = result)for reads - Use
Host.ServiceSubmitAsync(async () => await Service.Method())for writes - Do not manage
IsSubmitting/ loading state manually — BlazorToolkit managesHost.InProgress - Use
Host.InProgress,Host.IsError,Host.ErrorMessagefor UI state - Do not inject
ApplicationDbContext,IUserStore,ITimeProvider, orIBackgroundWorkerinto pages
- Define an interface (
I{Entity}Service) for each service - Inherit from
BaseService - Annotate with
[BlazorService] - Return
ServiceActionResult<T>(useServiceActionResult<T>.OK(data)) - Open one unit of work per public method —
await using var repo = RepositoryFactory.Create();— then access data viarepo.GetXxxQuery(AuthorizationContext.CurrentProfile). There is no shared scopedRepositoryproperty onBaseServiceany more; it was removed because concurrent Blazor components on one circuit collided on the shared context. Private helpers that touch data take anIQueryRepository repoparameter instead of creating their own. See../../Database/UnitOfWork.md. - Create new records via
query.CreateNew()+entity.ToRecord(dto)+query.AddAsync(record) - Update existing records via
entity.ToRecord(dto)+query.UpdateAsync(record) - Background work (e.g., email) via
IBackgroundWorker.Submit() - DI registration is automatic —
[BlazorService]+AddBlazorServices()registers both the concrete type and its interfaces
- Create a local logger in the constructor:
log = logManager.CreateLogger(this); - Start each method with a trace scope:
using var l = log.TraceScope(); - Use shorthand methods on the scope:
l.I("info message"),l.E("error message") - Do not use
ILogger/LogInformation/LogError— useIScopeLogfromDevInstance.LogScope
- Use
IdGenerator.New()fromDevInstance.WebServiceToolkit.Common.Toolsfor generating unique public IDs and temporary values (e.g., temp passwords)
- DTOs (
{Entity}Item) carry validation attributes ([Required],[EmailAddress],[Phone],[Display]) directly — no separateInputModelclasses in pages - Pages bind forms directly to the DTO:
[SupplyParameterFromForm] private UserProfileItem Input { get; set; } = new(); - Fields not part of the DTO (e.g., role selection during creation) live as separate page properties
- DTO / View Model:
{Entity}Item(e.g.,UserProfileItem) - Service Interface:
I{Entity}Service(e.g.,IUserProfileService) - Service Implementation:
{Entity}Service(e.g.,UserProfileService) - Service Mock:
{Entity}ServiceMock(e.g.,UserProfileServiceMock) - Decorators:
{Entity}Decorators— extension methodsToView()/ToRecord()for model ↔ DTO conversionToView()converts database model → DTOToRecord()maps DTO fields onto an existing database entity
- Database Model:
{Entity}inheritingDatabaseObject
Queue emails via IBackgroundWorker.Submit() with a BackgroundRequestItem of type SendEmail containing an EmailRequest.
- HTML templates live in
wwwroot/email-templates/ - Template names are string constants in
EmailTemplateName - Template metadata (subject, path, isHtml) is registered in
EmailTemplateRepository - Render templates via
IEmailTemplateService.RenderAsync(name, placeholders)— returnsEmailTemplateResultwithSubject,Content,IsHtml - Placeholders use
{{Key}}syntax in both subject and body - To add a new template: add a constant to
EmailTemplateName, register inEmailTemplateRepository, create the HTML file inwwwroot/email-templates/
Use HDataGrid<TItem> for all tabular data pages. Do not write inline <table> markup. Full documentation: Core/UI/Components/HDataGrid.md.
Generic CSV/Excel import and export for any entity type via handler pattern. Full documentation: ../../Services/Core/ImportExport/ImportExport.md.
Defined in ApplicationRoles: Owner, Admin, Manager, Employee, Client. Owner is the super-admin role and is typically excluded from user-assignable roles.
Service mocks allow running the application without a real database or external dependencies. They are used for UI development and testing.
- UI development — Iterate on pages without needing a database, Identity, or email infrastructure
- Predictable data — Mocks generate consistent fake data via the Bogus library
- Isolation — Test UI behavior independently from backend logic
The solution has a ServiceMocks build configuration. Use it to run with mock services:
- Visual Studio: Select
ServiceMocksfrom the configuration dropdown - CLI:
dotnet build -c ServiceMocks/dotnet run -c ServiceMocks
The SERVICEMOCKS preprocessor symbol controls which services are registered in Program.cs:
#if !SERVICEMOCKS
builder.Services.AddBlazorServices(); // registers [BlazorService] classes
builder.Services.AddBlazorServices(typeof(UserProfileService).Assembly);
#else
builder.Services.AddBlazorServicesMocks(); // registers [BlazorServiceMock] classes
builder.Services.AddBlazorServicesMocks(typeof(UserProfileServiceMock).Assembly); // from mocks assembly
builder.Services.AddBlazorServicesMocks(typeof(UserProfileService).Assembly); // dual-annotated services from real assembly
#endifmocks/Server/Admin/ServicesMocks/
├── DevCoreApp.Admin.Services.Mocks.csproj # References real services project + Bogus
├── UserAdmin/
│ └── UserProfileServiceMock.cs # Mock for IUserProfileService
└── Email/
└── EmailLogServiceMock.cs # Mock for IEmailLogService
- Create
{Entity}ServiceMock.csin the appropriate subfolder undermocks/Server/Admin/ServicesMocks/ - Implement the service interface (e.g.,
IUserProfileService) - Annotate with
[BlazorServiceMock](not[BlazorService]) - Generate fake data in the constructor using Bogus
Faker<T> - Store data in an in-memory
List<T>and operate on it - Add
await Task.Delay(delay)in async methods to simulate latency
[BlazorServiceMock]
public class UserProfileServiceMock : IUserProfileService
{
const int TotalCount = 100;
List<UserProfileItem> modelList;
private int delay = 500;
public UserProfileServiceMock()
{
var faker = new Faker<UserProfileItem>()
.RuleFor(u => u.Id, f => IdGenerator.New())
.RuleFor(u => u.Email, f => f.Internet.Email())
.RuleFor(u => u.FirstName, f => f.Name.FirstName())
.RuleFor(u => u.LastName, f => f.Name.LastName());
modelList = faker.Generate(TotalCount);
}
// Implement interface methods operating on modelList...
}Services that have no mock and should work in both modes (e.g., GridProfileService, AccountService) must carry both attributes:
[BlazorService]
[BlazorServiceMock]
public class GridProfileService : BaseService { ... }This ensures they are registered by both AddBlazorServices() and AddBlazorServicesMocks().