A public, end-to-end demonstration of use-case-oriented orchestration in .NET.
This repository shows how DataArc can keep business policy, application coordination, persistence mechanics, observability, and presentation concerns separate without requiring every use case to grow into a collection of repositories, handlers, commands, queries, and persistence-aware services.
The demo uses multiple EF Core DbContext boundaries over a single physical SQL Server database and coordinates one employee-onboarding use case across Identity, HR, Finance, IT, and Operations.
The goal is not to remove architectural boundaries. The goal is to stop creating abstractions simply because a pattern says they must exist.
A dedicated orchestration layer helps avoid:
- Repositories for repository's sake
- Repositories for every database or persistence permutation
- CQRS + command/query + handler proliferation where the use case does not need it
- Multiple command/query classes for every database-column or persistence permutation
- Handler and event-handler sprawl
- Application services that know too much about EF Core, transactions, connections, or save order
Traditional application-layer plumbing can become surprisingly large when a single use case needs to read state, apply business rules, write to several persistence boundaries, emit events, and commit atomically.
DataArc takes a different approach:
A use case may query and command within the same explicit orchestrator when that is what the use case actually requires.
The orchestrator coordinates persistence.
The domain owns business rules.
The application service coordinates the use case.
Ports isolate the application from orchestration implementation.
Observers provide logs, traces, metrics, and other reactions to domain decisions.
That keeps the code aligned with the business operation rather than forcing the business operation to align with a plumbing pattern.
The employee-onboarding use case crosses five application persistence boundaries:
- Auth — resolves the existing Identity user
- HR — creates the employee and department relationship
- Finance — creates the payroll record
- IT — creates the access request
- Operations — creates the onboarding task
The use case is intentionally implemented as a single in-process, transactional workflow.
If the execution succeeds, the transaction commits once.
If a persistence or system failure occurs, the transaction is rolled back and the exception propagates as a technical failure.
Business rejection is handled earlier by domain policy and is not represented as a failed persistence operation.
flowchart TD
UI[Web UI] --> API[Web API]
API --> SERVICE[EmployeeOnboardingService]
SERVICE --> PREP[Prepare Orchestration]
PREP --> PORT[IHROrchestrationPort]
SERVICE --> DOMAIN[Rich Domain Value Object]
DOMAIN --> POLICY[OnboardEmployeePolicy]
POLICY --> RESULT[PolicyResult]
RESULT --> OBS[Observer Pipeline]
OBS --> TELEMETRY[Logs / Traces / Metrics]
RESULT -->|Accepted| EXEC[Execution Orchestration]
RESULT -->|Rejected| API
EXEC --> AUTH[(Auth DbContext)]
EXEC --> HR[(HR DbContext)]
EXEC --> FIN[(Finance DbContext)]
EXEC --> IT[(IT DbContext)]
EXEC --> OPS[(Operations DbContext)]
AUTH --> DB[(Single SQL Server Database)]
HR --> DB
FIN --> DB
IT --> DB
OPS --> DB
The important boundary is:
Application
↓
Port
↓
Orchestration
↓
Persistence
The application layer does not need to know about DbContext, shared SQL connections, transaction ownership, save order, rollback mechanics, or how many persistence boundaries participate in the use case.
The service coordinates the use case without owning persistence mechanics or business rules.
Conceptually, the flow is:
var preparedEmployee =
await _hrOrchestration.PrepareEmployeeOnboardingAsync(...);
var policyContext =
new OnboardEmployeePolicyContext(
new OnBoardEmployeeValueObject(...));
var policyResult =
_employeeOnboardingPolicy.Apply(policyContext);
await _observableEventHandler.DispatchAsync(
policyResult.DomainEvents);
if (!policyResult.IsSuccess)
{
return new OnboardEmployeeResponseDto
{
IsSuccess = false,
FailureReason = policyResult.Message
};
}
var output =
await _hrOrchestration.OnboardEmployeeAsync(
new OnboardEmployeeInput(...));
return new OnboardEmployeeResponseDto
{
IsSuccess = true,
EmployeeId = output.EmployeeId,
PayrollRecordId = output.PayrollRecordId
};There are three deliberately different outcomes:
Validation failure
→ the request itself is invalid
Policy rejection
→ the request is valid, but a business rule rejects the operation
System / persistence failure
→ an exception propagates from execution
Those concepts are not collapsed into one generic "failed" path.
The orchestration layer gathers facts. It does not decide whether onboarding is allowed.
Prepared persistence state is translated into a rich domain value object, and the policy makes the business decision.
Examples of policy decisions in the demo include:
- the Identity user must exist;
- the employee must not already be onboarded;
- an employee must not already exist for the selected Identity user;
- onboarding records must not already exist for the same workflow.
The value object gives persisted facts domain meaning, while the policy decides whether those facts permit the use case.
This keeps business rules out of:
- controllers/endpoints;
- persistence code;
- EF Core entities;
- orchestrators;
- application plumbing.
A policy decision is useful to more than the caller.
The demo dispatches the domain events produced by the policy:
await _observableEventHandler.DispatchAsync(
policyResult.DomainEvents);Observers can then record the same business decision through:
- structured logs;
- traces;
- metrics;
- diagnostics;
- future auditing or integrations.
The UI receives the policy message, while Aspire/OpenTelemetry can expose the corresponding accepted or rejected decision operationally.
That means a business rejection is visible both to the user and to the system operator without duplicating the rule.
The application depends on an orchestration port rather than directly on orchestration implementation details.
For HR onboarding that boundary is represented by:
IHROrchestrationPort
The application asks for a use case to be prepared or executed.
It does not need to know:
- which
DbContextis used; - how many contexts participate;
- how connections are shared;
- how the transaction is created;
- which entity is saved first;
- how rollback is performed.
That makes orchestration an explicit application capability rather than persistence logic leaking into application services.
The execution orchestrator receives an already-approved instruction.
It does not decide:
- whether onboarding is allowed;
- whether the employee should exist;
- whether the user qualifies;
- what business rule should reject the operation.
It coordinates persistence.
The onboarding transaction creates:
Employee
EmployeeDepartment
PayrollRecord
AccessRequest
OnboardingTask
The different EF Core contexts represent application boundaries while targeting the same physical relational database.
The orchestrator coordinates those boundaries using one local transaction and one final commit.
The orchestrator is not responsible for inventing business state such as:
Employee onboarding status
Access level
Access-request status
Onboarding-task name
Onboarding-task status
Payroll activation
Effective date
Due date
Those values are supplied through the orchestration input.
The execution orchestrator persists the approved instruction rather than making new business decisions while saving it.
This makes future policy changes possible without redesigning the persistence workflow.
The onboarding flow is intentionally simple:
Prepare
↓
Policy
↓
Execute
↓
Commit
It does not currently provide:
- persisted workflow instances;
- checkpoint/resume;
- retry scheduling;
- long-running workflow state;
- compensation orchestration;
- process recovery after host termination.
The transaction either commits or rolls back.
Downstream records such as an access request or onboarding task may then continue through their own lifecycles independently.
For example:
Employee onboarding: Completed
Access request: Requested
Onboarding task: Created
Payroll record: Active
Completed means the onboarding use case itself committed successfully. It does not imply that every downstream activity created by onboarding has already completed.
You will need:
- the .NET SDK targeted by the solution;
- SQL Server;
- a connection string configured for the demo database;
- NuGet access to the DataArc packages referenced by the solution.
The web demo and the DDL Builder integration-test path intentionally have different setup stories.
Before starting the application, run:
setup/SetupDemoDatabase.sql
against your SQL Server instance.
The supplied SQL script is the supported setup path for running the Web API and Web UI demo.
The application intentionally fails fast when the demo database is unavailable rather than silently creating application infrastructure at runtime.
The persistence modules use the shared connection named:
DataArcDemoDb
Update the relevant application settings for your SQL Server environment.
All five DbContext boundaries target the same physical demo database.
Run the solution through the Aspire AppHost.
The Aspire dashboard is useful for this demo because the observer pipeline exposes policy decisions through application telemetry.
You can watch accepted and rejected onboarding decisions alongside the running services.
Employee onboarding begins with an existing Identity user.
Use the Import Users feature in the demo UI to create demo Identity users before onboarding them.
The onboarding API accepts the Identity username rather than exposing the Identity database key as a public input contract.
The preparation orchestrator resolves that username to the persisted Identity user before policy evaluation.
Open the employee-onboarding page and submit an existing Identity username.
The demo will:
- resolve the Identity user;
- gather onboarding state across the participating persistence boundaries;
- build the domain policy context;
- evaluate the onboarding policy;
- dispatch accepted/rejected domain events to observers;
- if accepted, execute the transactional persistence workflow;
- create the related HR, Finance, IT, and Operations records;
- commit once.
A policy rejection is surfaced to the UI as meaningful business information rather than as a generic validation message.
The repository intentionally keeps the integration test small because it also serves as a product demonstration.
The integration-test setup exercises the commercial SQL Server tooling in:
DataArc.EntityFrameworkCore.SqlServer
The test registers the complete demo and composes the physical database from multiple EF Core contexts.
Conceptually:
var databaseBuilder =
_databaseFactory.CreateDatabaseBuilder();
var demoDatabase =
databaseBuilder
.IncludeDbContext<AuthDbContext>()
.IncludeDbContext<HrDbContext>()
.IncludeDbContext<ItDbContext>()
.IncludeDbContext<OperationsDbContext>()
.IncludeDbContext<FinanceDbContext>()
.Build(
generateScripts: true,
applyChanges: true);
demoDatabase.ExecuteDrop();
demoDatabase.ExecuteCreate();This demonstrates:
- multi-
DbContextrelational composition; - database creation through DataArc;
- generated SQL scripts;
- the complete onboarding workflow against a real SQL Server database.
Important: the integration test is destructive by design. Point it at a dedicated disposable integration-test database, never at the database used by the running Web UI/API demo.
The Web UI demo can be started from the supplied SQL setup script.
The integration-test DDL Builder path is different: it demonstrates commercial SQL Server functionality and therefore requires a valid DataArc key or configured server key.
That separation is intentional:
Web demo
→ supplied SQL script
→ no DDL Builder required for setup
Integration test
→ DataArc SQL Server DDL Builder
→ commercial key required
The integration-test project keeps generated DDL scripts visible in the project structure.
Its project file includes:
<ItemGroup>
<Folder Include="bin\Debug\net10.0\Scripts\" />
<Folder Include="Database\Scripts\" />
</ItemGroup>This allows the SQL generated by the DDL Builder to remain visible as part of the demonstration rather than disappearing into build output.
Contains:
- feature services;
- domain policies;
- rich domain value objects;
- domain events;
- module registration;
- application contracts.
Application services coordinate use cases but do not perform EF Core persistence.
Contains:
- orchestrator contracts;
- orchestrator inputs and outputs;
- orchestration ports/adapters;
- explicit persistence coordination.
This is where persistence workflow mechanics live.
It is deliberately separate from the application layer.
Contains:
- EF Core
DbContextimplementations; - persistence models;
- persistence-module registration.
Each persistence boundary registers its own context factory while participating in the host-managed logging pipeline.
Maps application outcomes to HTTP semantics.
For example:
Successful onboarding
→ 200 OK
Business-policy rejection
→ 422 Unprocessable Entity
Technical/system failure
→ exception / server error
The feature service itself does not depend on HTTP concepts.
Provides a simple UI for exercising the demo.
Its purpose is not to hide the architecture. It makes the architecture visible:
- import Identity users;
- trigger onboarding;
- see successful persisted outcomes;
- see meaningful policy rejections;
- compare UI behavior with Aspire logs, traces, and metrics.
Repositories remain valid when they provide a meaningful domain abstraction.
This demo intentionally shows that they are not required merely to create a persistence boundary.
Without an explicit orchestration layer, a multi-context use case can easily produce:
Application Service
→ Repository A
→ Repository B
→ Repository C
→ Command A
→ Command Handler A
→ Query B
→ Query Handler B
→ Event C
→ Event Handler C
→ Transaction coordination somewhere else
The use case becomes fragmented across plumbing classes.
DataArc instead permits the persistence workflow to remain explicit:
Application
→ Domain policy
→ Port
→ Orchestrator
→ Query what the use case needs
→ Command what the use case needs
→ Coordinate persistence boundaries
→ Commit once
The important principle is not "never use repositories."
It is:
Do not create repositories, handlers, commands, or queries solely to satisfy architectural ceremony when an explicit use-case orchestrator expresses the operation more clearly.
CQRS is valuable when separate read and write models solve an actual problem.
It becomes expensive when every small application interaction is mechanically decomposed into:
Command
Command Handler
Query
Query Handler
Event
Event Handler
and then multiplied again for every persistence or column permutation.
The orchestration framework does not prohibit CQRS.
It simply does not require a use case to pretend that its reads and writes are unrelated when they belong to one coherent operation.
An orchestrator may query the state it needs and execute the commands required by the same approved use case.
Domain events remain useful in this demo.
The distinction is that events are used deliberately.
The onboarding policy emits an accepted or rejected domain event because that decision is valuable to observers.
The observer pipeline can then provide telemetry without turning every persistence step into another application handler.
This keeps events focused on meaningful domain or operational signals instead of using them as mandatory plumbing between every line of a use case.
The demo deliberately separates business failure from system failure.
A valid request may still violate a business rule.
The domain policy returns a rejection and a meaningful reason.
That reason is visible to the caller, and the corresponding domain event is visible to observers.
A SQL error, connection failure, unexpected persistence problem, or other technical failure is not converted into a fake business result.
The execution orchestrator rolls back the transaction and allows the exception to propagate.
This prevents technical failures from masquerading as domain decisions.
EF Core uses the host-managed Microsoft.Extensions.Logging pipeline.
Persistence modules do not create private static LoggerFactory instances.
Typed application/orchestration loggers, EF Core diagnostics, and observer telemetry therefore participate in the same host logging infrastructure and can be surfaced through Aspire/OpenTelemetry.
The public integration test is intentionally small.
Its purpose is to prove the complete path and demonstrate the DataArc SQL Server tooling:
Application service
→ Domain policy
→ Port
→ Orchestration
→ Multiple EF Core contexts
→ SQL Server
The demo avoids turning the test project into a testing-framework showcase.
The product and architectural flow remain the focus.
The repository demonstrates the roles of the DataArc stack:
Explicit input/output application orchestration without handler-per-message ceremony.
In-process domain-event dispatch with a natural hook for logs, traces, metrics, auditing, and other reactions.
EF Core-oriented persistence capabilities used by the demo.
Commercial SQL Server functionality including the database-definition / DDL Builder path demonstrated by the integration test.
The demo intentionally follows these rules:
- Business rules belong in the domain.
- Persistence facts may inform policy, but persistence does not make policy decisions.
- Application services coordinate; they do not become persistence layers.
- Ports isolate the application from orchestration implementation.
- Execution orchestrators coordinate persistence and do not invent business rules.
- Business rejection and technical failure are different concepts.
- Observers react to domain decisions without coupling telemetry to policy.
- Multiple
DbContextboundaries do not require repository proliferation. - Queries and commands may coexist in one orchestrator when they are part of one coherent use case.
- Architecture should reduce plumbing, not manufacture it.
The repository is deliberately focused.
It demonstrates one clear cross-module use case well rather than attempting to become:
- a generic workflow engine;
- a repository framework;
- a CQRS framework;
- a mediator replacement for every message in the application;
- a complete HR system.
The point is to show how explicit orchestration, domain policy, observation, and persistence boundaries can work together while keeping the application understandable.
The orchestration layer is not there to move business logic out of the domain.
It is there to give cross-boundary execution mechanics a proper home.
That leaves each layer with a clearer job:
Domain
→ decides
Application
→ coordinates
Observation
→ reacts and records
Orchestration
→ executes the approved persistence workflow
Persistence
→ stores state
Presentation
→ communicates outcomes
That is the architecture this demo is intended to make visible.