Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/examples/legacy-order-anti-corruption-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Legacy Order Anti-Corruption Layer

This example models a legacy ERP order feed imported into a commerce domain. The anti-corruption layer keeps legacy DTO shape, currency codes, whitespace, and customer identifiers out of the domain model.

It demonstrates:

- a fluent `AntiCorruptionLayer<LegacyOrderDto, CommerceOrder>`
- a source-generated anti-corruption layer factory
- an `IServiceCollection` extension that imports the demo into a standard .NET host

```csharp
var services = new ServiceCollection();
services.AddLegacyOrderAntiCorruptionDemo();

using var provider = services.BuildServiceProvider();
var importer = provider.GetRequiredService<LegacyOrderImportService>();

var result = await importer.ImportAsync("ORD-100");
```

Applications can replace `ILegacyOrderFeed` with their own gateway while keeping the generated layer registration. The accompanying TinyBDD tests validate accepted imports, rejected model drift, and DI composition.
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
- name: Template Method (Async)
href: template-method-async-demo.md

- name: Legacy Order Anti-Corruption Layer
href: legacy-order-anti-corruption-layer.md

- name: Inventory Retry Policy
href: inventory-retry-policy.md

Expand Down
36 changes: 36 additions & 0 deletions docs/generators/anti-corruption-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Anti-Corruption Layer Generator

The anti-corruption layer generator creates a strongly typed `AntiCorruptionLayer<TExternal, TDomain>` factory from declarative translator and validation attributes.

```csharp
[GenerateAntiCorruptionLayer(
typeof(LegacyOrderDto),
typeof(CommerceOrder),
FactoryMethodName = "CreateGeneratedLayer",
LayerName = "legacy-order-import",
SourceSystem = "legacy-erp")]
public static partial class LegacyOrderAcl
{
[AntiCorruptionTranslator]
private static CommerceOrder Translate(LegacyOrderDto order)
=> new(order.OrderNumber.Trim(), order.GrossAmount, order.CustomerCode.Trim());

[AntiCorruptionExternalRule("Only USD orders are imported.")]
private static bool IsUsd(LegacyOrderDto order) => order.CurrencyCode == "USD";

[AntiCorruptionDomainRule("Imported order total must be positive.")]
private static bool HasPositiveTotal(CommerceOrder order) => order.TotalUsd > 0m;
}
```

The generated factory returns the same runtime layer as the fluent API.

## Rules

- The host type must be `partial`.
- Exactly one `[AntiCorruptionTranslator]` method is required.
- The translator must be `static`, return the domain type, and accept one external model parameter.
- External rules must be `static bool` methods with one external model parameter.
- Domain rules must be `static bool` methods with one domain model parameter.

Diagnostics use the `PKACL` prefix.
5 changes: 5 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**State Machine**](state-machine.md) | Deterministic finite state machines | `[StateMachine]` |
| [**Strategy**](strategy.md) | Predicate-based dispatch with fluent builder | `[GenerateStrategy]` |
| [**Specification**](specification.md) | Named business-rule registries | `[GenerateSpecificationRegistry]` |
| [**Anti-Corruption Layer**](anti-corruption-layer.md) | External-to-domain translation boundaries with validation | `[GenerateAntiCorruptionLayer]` |
| [**Template Method**](template-method-generator.md) | Template method skeletons with hook points | `[Template]` |
| [**Visitor**](visitor-generator.md) | Type-safe visitor implementations | `[GenerateVisitor]` |

Expand Down Expand Up @@ -154,6 +155,10 @@ public static partial class PricingRules { }
[Template]
public abstract partial class DataProcessor { }

// Anti-corruption layer - external model boundary
[GenerateAntiCorruptionLayer(typeof(LegacyOrderDto), typeof(CommerceOrder))]
public static partial class LegacyOrderAcl { }

// Visitor - type-safe double dispatch
[GenerateVisitor]
public interface IDocumentVisitor { }
Expand Down
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- name: Adapter
href: adapter.md

- name: Anti-Corruption Layer
href: anti-corruption-layer.md

- name: Builder
href: builder.md

Expand Down
1 change: 1 addition & 0 deletions docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Cloud Architecture | Rate Limiting | `RateLimitPolicy<T>` | Rate Limiting generator |
| Application Architecture | CQRS | Mediator/dispatcher command-query split | Dispatcher generator |
| Application Architecture | Specification | `Specification<T>` and named registries | Specification generator |
| Application Architecture | Anti-Corruption Layer | `AntiCorruptionLayer<TExternal, TDomain>` | Anti-Corruption Layer generator |

## Research Baselines

Expand Down
29 changes: 29 additions & 0 deletions docs/patterns/application/anti-corruption-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Anti-Corruption Layer

An Anti-Corruption Layer protects a domain model from external schemas, naming, invariants, and operational shortcuts. It validates the external model, translates only accepted inputs, then validates the resulting domain model before anything downstream observes it.

PatternKit provides `AntiCorruptionLayer<TExternal, TDomain>` in `PatternKit.Application.AntiCorruption`.

```csharp
var layer = AntiCorruptionLayer<LegacyOrderDto, CommerceOrder>
.Create("legacy-order-import")
.FromSource("legacy-erp")
.RequireExternal(static order => order.CurrencyCode == "USD", "Only USD orders are imported.")
.TranslateWith(static order => new CommerceOrder(order.OrderNumber.Trim(), order.GrossAmount, order.CustomerCode.Trim()))
.RequireDomain(static order => order.TotalUsd > 0m, "Imported order total must be positive.")
.Build();

var result = layer.Translate(legacyOrder);
```

The result reports whether the import was accepted, the source system, the protected domain value, and any rejection reason.

## Production Notes

- Keep external DTOs outside the domain model and translate at the application boundary.
- Use external validation for schema drift, unsupported values, missing identifiers, and source-specific quirks.
- Use domain validation for invariants that must remain true after mapping and normalization.
- Return explicit rejection reasons so ingestion pipelines can route invalid input to diagnostics or remediation.
- Register the layer through DI beside the gateway/feed that reads the external system.

The legacy order example shows fluent and source-generated layer creation plus `IServiceCollection` registration for importing applications.
6 changes: 6 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@
href: cloud/cache-aside.md
- name: Rate Limiting
href: cloud/rate-limiting.md
- name: Application Architecture
items:
- name: Anti-Corruption Layer
href: application/anti-corruption-layer.md
- name: Specification
href: application/specification.md
- name: Type-Dispatcher
href: behavioral/type-dispatcher/index.md
items:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
namespace PatternKit.Application.AntiCorruption;

/// <summary>
/// Outcome returned by an anti-corruption layer translation.
/// </summary>
public sealed class AntiCorruptionResult<TDomain>
{
private AntiCorruptionResult(string sourceSystem, TDomain? value, bool accepted, string? rejectionReason)
{
SourceSystem = sourceSystem;
Value = value;
Accepted = accepted;
RejectionReason = rejectionReason;
}

public string SourceSystem { get; }
public TDomain? Value { get; }
public bool Accepted { get; }
public bool Rejected => !Accepted;
public string? RejectionReason { get; }

public static AntiCorruptionResult<TDomain> Success(string sourceSystem, TDomain value)
=> new(sourceSystem, value, true, null);

public static AntiCorruptionResult<TDomain> Rejection(string sourceSystem, string reason)
=> new(sourceSystem, default, false, reason);
}

/// <summary>
/// Translates external models into a protected domain model while rejecting invalid external or domain shapes.
/// </summary>
public sealed class AntiCorruptionLayer<TExternal, TDomain>
{
public delegate TDomain Translator(TExternal external);
public delegate bool Validator<in T>(T value);

private readonly Translator _translator;
private readonly IReadOnlyList<ValidationRule<TExternal>> _externalRules;
private readonly IReadOnlyList<ValidationRule<TDomain>> _domainRules;

private AntiCorruptionLayer(
string name,
string sourceSystem,
Translator translator,
IReadOnlyList<ValidationRule<TExternal>> externalRules,
IReadOnlyList<ValidationRule<TDomain>> domainRules)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Anti-corruption layer name is required.", nameof(name));
if (string.IsNullOrWhiteSpace(sourceSystem))
throw new ArgumentException("Source system is required.", nameof(sourceSystem));

Name = name;
SourceSystem = sourceSystem;
_translator = translator ?? throw new ArgumentNullException(nameof(translator));
_externalRules = externalRules ?? throw new ArgumentNullException(nameof(externalRules));
_domainRules = domainRules ?? throw new ArgumentNullException(nameof(domainRules));
}

public string Name { get; }
public string SourceSystem { get; }

public static Builder Create(string name = "anti-corruption-layer") => new(name);

public AntiCorruptionResult<TDomain> Translate(TExternal external)
{
if (external is null)
throw new ArgumentNullException(nameof(external));

foreach (var rule in _externalRules)
{
if (!rule.Predicate(external))
return AntiCorruptionResult<TDomain>.Rejection(SourceSystem, rule.RejectionReason);
}

var domain = _translator(external);
if (domain is null)
return AntiCorruptionResult<TDomain>.Rejection(SourceSystem, "Translator returned a null domain value.");

foreach (var rule in _domainRules)
{
if (!rule.Predicate(domain))
return AntiCorruptionResult<TDomain>.Rejection(SourceSystem, rule.RejectionReason);
}

return AntiCorruptionResult<TDomain>.Success(SourceSystem, domain);
}

public async ValueTask<AntiCorruptionResult<TDomain>> TranslateAsync(
TExternal external,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return await new ValueTask<AntiCorruptionResult<TDomain>>(Translate(external)).ConfigureAwait(false);
Comment on lines +89 to +94
}

public sealed class Builder
{
private readonly string _name;
private readonly List<ValidationRule<TExternal>> _externalRules = [];
private readonly List<ValidationRule<TDomain>> _domainRules = [];
private string _sourceSystem = "external";
private Translator? _translator;

internal Builder(string name) => _name = name;

public Builder FromSource(string sourceSystem)
{
_sourceSystem = sourceSystem;
return this;
}

public Builder TranslateWith(Translator translator)
{
_translator = translator ?? throw new ArgumentNullException(nameof(translator));
return this;
}

public Builder RejectExternalWhen(Validator<TExternal> predicate, string reason)
=> AddExternalRule(value => !predicate(value), reason);

public Builder RequireExternal(Validator<TExternal> predicate, string reason)
=> AddExternalRule(predicate, reason);

public Builder RejectDomainWhen(Validator<TDomain> predicate, string reason)
=> AddDomainRule(value => !predicate(value), reason);
Comment on lines +120 to +126

public Builder RequireDomain(Validator<TDomain> predicate, string reason)
=> AddDomainRule(predicate, reason);

public AntiCorruptionLayer<TExternal, TDomain> Build()
{
if (_translator is null)
throw new InvalidOperationException("Anti-corruption layer translator is required.");

return new(_name, _sourceSystem, _translator, _externalRules.ToArray(), _domainRules.ToArray());
}

private Builder AddExternalRule(Validator<TExternal> predicate, string reason)
{
_externalRules.Add(new(predicate ?? throw new ArgumentNullException(nameof(predicate)), RequireReason(reason)));
return this;
}

private Builder AddDomainRule(Validator<TDomain> predicate, string reason)
{
_domainRules.Add(new(predicate ?? throw new ArgumentNullException(nameof(predicate)), RequireReason(reason)));
return this;
}

private static string RequireReason(string reason)
=> string.IsNullOrWhiteSpace(reason)
? throw new ArgumentException("Validation rejection reason is required.", nameof(reason))
: reason;
}

private sealed class ValidationRule<T>
{
public ValidationRule(Validator<T> predicate, string rejectionReason)
{
Predicate = predicate;
RejectionReason = rejectionReason;
}

public Validator<T> Predicate { get; }
public string RejectionReason { get; }
}
}
Loading
Loading