Domain Policies¶
Encapsulate domain decisions as reusable, context-aware policy objects.
Overview¶
Domain policies model business decisions that should be evaluated against a domain context. They are useful when the logic is broader than a single property check or aggregate invariant and you want to express it as a reusable, composable unit.
In bITdevKit, a domain policy:
- implements
IDomainPolicy<TContext> - can decide whether it applies to a given context through
IsEnabledAsync(...) - returns an
IResultfromApplyAsync(...) - can be executed alone or as part of a policy set through
DomainPolicies.ApplyAsync(...)
The feature is meant for domain-level decision logic, not infrastructure authorization policies and not low-level fluent validation. It complements Rules and Results rather than replacing them.
Challenges¶
Some business decisions depend on several domain objects and need more context than a single predicate. Callers may also need to skip inapplicable decisions, aggregate several failures, stop at the first failure, or retain a typed output from each decision.
Solution¶
An IDomainPolicy<TContext> separates applicability from evaluation. DomainPolicies.ApplyAsync
executes enabled policies in order and produces a DomainPolicyResult<TContext> with combined
messages, errors, and per-policy values.
Key Features¶
- Asynchronous applicability and evaluation
- Reusable context-specific policy classes
- Continue, stop, or throw processing modes
- Aggregated messages and errors
- Typed per-policy values keyed by policy type
- Compatibility with the shared
IResultcontract
Architecture¶
IDomainPolicy<TContext> defines IsEnabledAsync and ApplyAsync.
DomainPolicyBase<TContext> enables a policy by default. DomainPolicies evaluates policy arrays
sequentially, combines each IResult, and records its value in DomainPolicyResults<TContext>.
DomainPolicyProcessingMode controls what happens after a failed policy.
Use Cases¶
Use domain policies when:
- a business decision spans multiple entities or value objects
- the check needs a dedicated context object rather than a single input value
- you want to compose several policy checks into one domain decision
- each policy may return its own typed result value
- the caller needs control over whether evaluation should continue, stop, or throw on failure
Typical examples are eligibility checks, approval preconditions, order-placement constraints, or workflow transition rules.
Basic Usage¶
The following example applies one policy, checks the result before continuing, and prints the outcome:
var context = new CheckoutContext(CustomerIsActive: true);
var result = await DomainPolicies.ApplyAsync(
context,
new ActiveCustomerPolicy(),
CancellationToken.None);
if (result.IsFailure)
{
var details = result.Messages.Concat(result.Errors.Select(error => error.Message));
Console.Error.WriteLine(string.Join(Environment.NewLine, details));
return;
}
Console.WriteLine("Checkout policies passed.");
public sealed record CheckoutContext(bool CustomerIsActive);
public sealed class ActiveCustomerPolicy : DomainPolicyBase<CheckoutContext>
{
public override Task<IResult> ApplyAsync(
CheckoutContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult<IResult>(
context.CustomerIsActive
? Result.Success()
: Result.Failure().WithMessage("Customer must be active."));
}
}
Output:
Checkout policies passed.
Core building blocks¶
IDomainPolicy<TContext>¶
This is the core contract:
IsEnabledAsync(...)decides whether the policy should run for the supplied contextApplyAsync(...)performs the actual business check and returns anIResult
This separation allows a policy to be skipped for contexts where it does not apply.
DomainPolicyBase<TContext>¶
Most policies can inherit from DomainPolicyBase<TContext>, which already provides a default IsEnabledAsync(...) implementation returning true. You only override it when the policy should be skipped for some contexts.
DomainPolicies¶
DomainPolicies is the orchestration helper. It applies one or more policies to a context and produces a DomainPolicyResult<TContext> that aggregates:
- overall success or failure
- messages
- errors
- per-policy result values
DomainPolicyResult<TContext>¶
This result type follows the same explicit success-or-failure style as the general Result feature, but adds PolicyResults so callers can inspect the individual outputs of policies that ran.
DomainPolicyProcessingMode¶
When applying multiple policies, you can choose how failures are handled:
ContinueOnPolicyFailure: evaluate all enabled policies and aggregate the failuresStopOnPolicyFailure: stop at the first failure and return the partial resultThrowOnPolicyFailure: raise aDomainPolicyExceptionon the first failure
Policy definitions¶
Define a policy context¶
The context is the input model against which policies are evaluated:
public sealed record CheckoutContext(
Customer Customer,
Cart Cart,
Money Total);
Implement a policy¶
public sealed class CustomerMustBeActivePolicy : DomainPolicyBase<CheckoutContext>
{
public override Task<IResult> ApplyAsync(
CheckoutContext context,
CancellationToken cancellationToken = default)
{
if (context.Customer.Status != CustomerStatus.Active)
{
return Task.FromResult<IResult>(
Result.Failure().WithMessage("Customer must be active to check out"));
}
return Task.FromResult<IResult>(Result.Success());
}
}
Add a conditional policy¶
Policies can be enabled only for certain contexts:
public sealed class LargeOrderApprovalPolicy : DomainPolicyBase<CheckoutContext>
{
public override Task<bool> IsEnabledAsync(
CheckoutContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(context.Total.Amount >= 1000m);
}
public override Task<IResult> ApplyAsync(
CheckoutContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult<IResult>(
context.Customer.CanPlaceLargeOrders
? Result.Success()
: Result.Failure().WithMessage("Large orders require approval"));
}
}
Executing policies¶
Single policy¶
var result = await DomainPolicies.ApplyAsync(
context,
new CustomerMustBeActivePolicy(),
cancellationToken);
Multiple policies¶
var result = await DomainPolicies.ApplyAsync(
context,
[
new CustomerMustBeActivePolicy(),
new LargeOrderApprovalPolicy()
],
DomainPolicyProcessingMode.ContinueOnPolicyFailure,
cancellationToken);
If a policy is disabled through IsEnabledAsync(...), it is skipped and does not contribute to the aggregated result.
Returning policy-specific values¶
Policies are not limited to plain success/failure. Because ApplyAsync(...) returns IResult, a policy can return Result<T> or DomainPolicyResult<T>, and DomainPolicies stores the underlying value in PolicyResults.
That makes policies useful for decision outputs such as risk scores, approval levels, or computed limits.
If LargeOrderApprovalPolicy.ApplyAsync returns a Result<ApprovalLevel>, retrieve that value with:
var approvalLevel = result.PolicyResults
.GetValue<LargeOrderApprovalPolicy, ApprovalLevel?>();
This lets one policy set produce both validation failures and domain decision data.
Values are keyed by the concrete policy type. If the same policy type runs more than once in one call, the later value replaces the earlier value.
Failure handling¶
Continue¶
Use ContinueOnPolicyFailure when you want the caller to see all policy failures at once.
Stop¶
Use StopOnPolicyFailure when later policies depend on earlier policies being satisfied or when the first failure is enough.
Throw¶
Use ThrowOnPolicyFailure when a policy violation should escape as an exception. In that case the
framework throws DomainPolicyException, which carries the aggregated result and can be handled by
the presentation-layer exception handling documented in Exception Handling.
Domain policies and rules¶
Use domain policies when:
- the logic is contextual and decision-oriented
- the checks are substantial enough to deserve their own domain type
- you want optional execution via
IsEnabledAsync(...) - you want per-policy output values
Use Rules when:
- you want lightweight fluent validation
- the checks are mostly local predicates
- you want inline rule composition rather than named policy objects
A domain policy can use the Rules feature internally and return its Result.
Relationship to other features¶
- Domain covers aggregates, value objects, typed ids, and fluent aggregate changes.
- Results explains the result model used by domain policies.
- Rules covers the fluent rule engine that can complement policy implementations.
- Exception Handling documents how
DomainPolicyExceptioncan be translated into HTTP responses.