SaaS Domain Implementation Patterns¶
Wave-2 domain implementation patterns shared across all five ConnectSoft.Saas.<Context>Template bounded-context repositories. Use this guide when adding validators, business metrics, structured logging, or gRPC rich-error handling in DomainModel.Impl.
Reference implementation: ConnectSoft.Saas.EntitlementsTemplate — DefaultEntitlementsProcessor, DefaultEntitlementsRetriever, EntitlementsMetrics, and owned GrpcRichErrorInterceptor.
Per-repo inventories (validator filenames, instrumented operations, test commands) live in each template's docs/domain-implementation.md.
Repositories covered¶
| Repository | Aggregate root | Metrics class | Meter name | Validators |
|---|---|---|---|---|
ConnectSoft.Saas.BillingTemplate |
Subscription |
BillingMetrics |
ConnectSoft.Saas.Billing |
7 |
ConnectSoft.Saas.MeteringTemplate |
UsageMeter |
MeteringMetrics |
ConnectSoft.Saas.Metering |
8 |
ConnectSoft.Saas.TenantsTemplate |
Tenant |
TenantsMetrics |
ConnectSoft.Saas.Tenants |
8 |
ConnectSoft.Saas.EntitlementsTemplate |
Entitlement |
EntitlementsMetrics |
ConnectSoft.Saas.Entitlements |
7 |
ConnectSoft.Saas.ProductsCatalogTemplate |
Product |
ProductsCatalogMetrics |
ConnectSoft.Saas.ProductsCatalog |
9 |
FluentValidation¶
Location and shape¶
- Validators live in
src/ConnectSoft.Saas.<Context>.DomainModel.Impl/Validators/. - One public validator class per file, with XML documentation on the class and rules.
- Validators are 1:1 with processor/retriever input types (e.g.
CreateEntitlementDraftInput→CreateEntitlementDraftInputValidator).
Processor / retriever usage¶
Processors and retrievers inject IValidator<TInput> and call ValidateAndThrowAsync before any domain work:
await this.createEntitlementDraftInputValidator.ValidateAndThrowAsync(input, token).ConfigureAwait(false);
Validation failures surface as FluentValidation.ValidationException, which the owned gRPC interceptor maps to rich INVALID_ARGUMENT errors (see below).
Registration¶
In <Context>MicroserviceRegistration, register validators via assembly scan using a marker type:
validationOptions comes from AddConnectSoftFluentValidationOptions in OptionsExtensions.
Unit tests¶
- One
*ValidatorUnitTests.csper validator undertests/ConnectSoft.Saas.<Context>.UnitTests/DomainModel/Validators/. - Use FluentValidation's
TestValidatehelper; cover valid inputs and representative rule failures.
dotnet test tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj --filter "FullyQualifiedName~Validator"
Domain metrics¶
Location and naming¶
<Context>Metricsclass insrc/ConnectSoft.Saas.<Context>.Metrics/.- Meter name:
ConnectSoft.Saas.<Context>(constant<Context>MetricsMeterName). - Instrument naming convention:
- Counters:
{meter}.{operation}.{succeeded|failed} - Histograms:
{meter}.{operation}.duration(seconds) - Optional up/down counters:
{meter}.{aggregate}.total
Example (Entitlements):
ConnectSoft.Saas.Entitlements.entitlement.draft.created
ConnectSoft.Saas.Entitlements.entitlement.draft.create.failed
ConnectSoft.Saas.Entitlements.entitlement.draft.create.duration
Tags typically include module, component, aggregate, and tenant_id when known.
Registration¶
Override RegisterTemplateMetrics in <Context>MicroserviceRegistration:
protected override void RegisterTemplateMetrics(IServiceCollection services)
{
services.AddSingleton<EntitlementsMetrics>();
services.ActivateSingleton<EntitlementsMetrics>();
}
Wiring in domain handlers¶
Inject <Context>Metrics into Default*Processor and Default*Retriever. For each public operation:
- Start a
Stopwatchafter validation. - On success: record duration + succeeded counter (and up/down counter when applicable).
- On catch: record failed counter, then rethrow.
See DefaultEntitlementsProcessor.CreateDraftAsync in Entitlements for the canonical pattern.
Unit tests¶
Tests live in tests/.../Metrics/*MetricsUnitTests.cs. They use MetricCollector<TMetrics> and Microsoft.Extensions.Diagnostics.Testing to assert instrument names and tag dimensions without a live OTel pipeline.
dotnet test tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj --filter "FullyQualifiedName~Metrics"
Wave-2 totals: 34 metrics unit tests across the five repos (Entitlements 11, Billing 6, Metering 6, Tenants 5, Products Catalog 6).
Processor / retriever logging¶
Pattern from DefaultEntitlementsProcessor and DefaultEntitlementsRetriever (replicated in all five repos):
using (logger.BeginScopeWithFlow(...))— scope name isClassName/MethodName.logger.Here().LogInformationat operation start (with key identifiers).try/catch— on failure:LogErrorwith exception +Record*Failedmetrics; rethrow.- On success: domain log line +
Record*Succeededmetrics + completionLogInformation.
using (this.logger.BeginScopeWithFlow(nameof(DefaultEntitlementsProcessor) + "/" + nameof(this.CreateDraftAsync)))
{
try
{
this.logger.Here(log => log.LogInformation("Create entitlement draft for tenant {TenantId} started...", tenantId));
await this.validator.ValidateAndThrowAsync(input, token).ConfigureAwait(false);
// ... domain work ...
this.metrics.RecordDraftCreated(sw.Elapsed, input.TenantId);
this.logger.Here(log => log.LogInformation("Create entitlement draft for tenant {TenantId} successfully completed...", input.TenantId));
}
catch (Exception ex)
{
this.metrics.RecordDraftCreateFailed(tenantId);
this.logger.Here(log => log.LogError(ex, "Failed to create entitlement draft for tenant {TenantId}", tenantId));
throw;
}
}
Apply the same structure to every method on Default*Processor and Default*Retriever.
gRPC rich errors¶
Each SaaS template owns a context-specific GrpcRichErrorInterceptor in src/ConnectSoft.Saas.<Context>.ApplicationModel/. This is not the generic base-template interceptor — domain exception types and ErrorInfo.Reason codes are per bounded context.
Activation¶
Active when GrpcErrorHandlingStrategy == RichError in microservice options. Registered alongside gRPC hosting in ApplicationModel.
Exception mapping¶
The interceptor catches unhandled exceptions from gRPC unary handlers and maps them to Google.Rpc.Status rich errors embedded in RpcException trailers:
| Source | gRPC Code |
Notes |
|---|---|---|
ValidationException (FluentValidation) |
InvalidArgument |
Field-level BadRequest details |
| Domain not-found exceptions | NotFound |
Context-specific ErrorInfo.Reason |
| Conflict / already-exists exceptions | AlreadyExists |
|
| Tenant mismatch / permission | PermissionDenied |
|
| Status transition / precondition failures | FailedPrecondition |
|
| NHibernate / persistence failures | Internal or Unavailable |
Mapped per exception type |
ArgumentException / null arguments |
InvalidArgument |
See GrpcRichErrorInterceptor.CreateRpcException in Entitlements for the full mapping table.
PowerShell test quick reference¶
Run from the template repo root (examples use Entitlements; substitute project name per repo):
# All validator unit tests
dotnet test tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj --filter "FullyQualifiedName~Validator"
# All domain metrics unit tests
dotnet test tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj --filter "FullyQualifiedName~Metrics"
# Both filters in one run
dotnet test tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj --filter "FullyQualifiedName~Validator|FullyQualifiedName~Metrics"
| Repo | Unit test project |
|---|---|
| Billing | tests/ConnectSoft.Saas.Billing.UnitTests/ConnectSoft.Saas.Billing.UnitTests.csproj |
| Metering | tests/ConnectSoft.Saas.Metering.UnitTests/ConnectSoft.Saas.Metering.UnitTests.csproj |
| Tenants | tests/ConnectSoft.Saas.Tenants.UnitTests/ConnectSoft.Saas.Tenants.UnitTests.csproj |
| Entitlements | tests/ConnectSoft.Saas.Entitlements.UnitTests/ConnectSoft.Saas.Entitlements.UnitTests.csproj |
| Products Catalog | tests/ConnectSoft.Saas.ProductsCatalog.UnitTests/ConnectSoft.Saas.ProductsCatalog.UnitTests.csproj |
Related documents¶
- SaaS Template Baseline Checklist — §15 Domain implementation patterns (wave 2)
- Metrics and Options Extensibility —
IMetricsFeaturepattern vs SaaSRegisterTemplateMetrics - Per-repo
docs/domain-implementation.md— context-specific validator and metrics inventory