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
19 changes: 19 additions & 0 deletions docs/examples/order-repository-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Order Repository Pattern

The order repository example models an application persistence boundary that stores orders, queries through a `Specification<OrderRecord>`, rejects duplicate keys, and can be imported through `IServiceCollection`.

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

using var provider = services.BuildServiceProvider();
var workflow = provider.GetRequiredService<OrderRepositoryWorkflow>();
var summary = await workflow.RunAsync();
```
Comment on lines +5 to +12

The example includes fluent and source-generated repository factories plus TinyBDD coverage for add/get/query, duplicate handling, and DI import.

Files:

- `src/PatternKit.Examples/RepositoryDemo/OrderRepositoryDemo.cs`
- `test/PatternKit.Examples.Tests/RepositoryDemo/OrderRepositoryDemoTests.cs`
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
- name: Loan Approval Specifications
href: loan-approval-specifications.md

- name: Order Repository Pattern
href: order-repository-pattern.md

- name: Generated Mailbox
href: generated-mailbox.md

Expand Down
1 change: 1 addition & 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]` |
| [**Repository**](repository.md) | In-memory repository factories from key selectors | `[GenerateRepository]` |
| [**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
26 changes: 26 additions & 0 deletions docs/generators/repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Repository Generator

`[GenerateRepository]` emits a factory for `InMemoryRepository<TEntity,TKey>` from a partial host and a key selector method.

```csharp
using PatternKit.Generators.Repository;

[GenerateRepository(typeof(OrderRecord), typeof(string), FactoryName = "CreateRepository")]
public static partial class GeneratedOrderRepository
{
[RepositoryKeySelector]
private static string SelectKey(OrderRecord order) => order.OrderId;
}
```

## Diagnostics

| ID | Meaning |
| --- | --- |
| `PKREP001` | The host type marked with `[GenerateRepository]` must be partial. |
| `PKREP002` | The host must declare exactly one `[RepositoryKeySelector]` method. |
| `PKREP003` | The key selector must be static and return `TKey` from one `TEntity` parameter. |

## Example

See [Order Repository Pattern](../examples/order-repository-pattern.md).
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
- name: Rate Limiting
href: rate-limiting.md

- name: Repository
href: repository.md

- name: Retry
href: retry.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 @@ -67,6 +67,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 | Repository | `IRepository<TEntity,TKey>` and `InMemoryRepository<TEntity,TKey>` | Repository generator |
| Application Architecture | Anti-Corruption Layer | `AntiCorruptionLayer<TExternal, TDomain>` | Anti-Corruption Layer generator |

## Research Baselines
Expand Down
35 changes: 35 additions & 0 deletions docs/patterns/application/repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Repository

Repository gives application code a collection-like persistence boundary around domain entities. PatternKit's runtime repository is async-first, supports specification filtering, and has deterministic mutation results for duplicate adds and missing updates.

```csharp
var repository = InMemoryRepository<OrderRecord, string>
.Create(order => order.OrderId)
.UseComparer(StringComparer.OrdinalIgnoreCase)
.Build();

await repository.AddAsync(order, ct);
var pending = await repository.FindAsync(PendingOrderSpecification, ct);
```
Comment on lines +5 to +13

Use `IRepository<TEntity,TKey>` at application boundaries and keep durable persistence application-owned. `InMemoryRepository<TEntity,TKey>` is useful for tests, examples, and embedded adapters.

## Source Generator

`[GenerateRepository]` emits an in-memory repository factory from a static key selector:

```csharp
[GenerateRepository(typeof(OrderRecord), typeof(string), FactoryName = "CreateRepository")]
public static partial class GeneratedOrderRepository
{
[RepositoryKeySelector]
private static string SelectKey(OrderRecord order) => order.OrderId;
}
```

See [Repository Generator](../../generators/repository.md).

## Example

- `src/PatternKit.Examples/RepositoryDemo/OrderRepositoryDemo.cs`
- `test/PatternKit.Examples.Tests/RepositoryDemo/OrderRepositoryDemoTests.cs`
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@
items:
- name: Anti-Corruption Layer
href: application/anti-corruption-layer.md
- name: Repository
href: application/repository.md
- name: Specification
href: application/specification.md
- name: Type-Dispatcher
Expand Down
185 changes: 185 additions & 0 deletions src/PatternKit.Core/Application/Repository/Repository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using PatternKit.Application.Specification;

namespace PatternKit.Application.Repository;

/// <summary>
/// Async collection-like persistence boundary for domain entities.
/// </summary>
public interface IRepository<TEntity, TKey>
where TKey : notnull
{
/// <summary>Adds a new entity and rejects duplicate keys.</summary>
ValueTask<RepositoryResult<TEntity>> AddAsync(TEntity entity, CancellationToken cancellationToken = default);

/// <summary>Gets an entity by key.</summary>
ValueTask<TEntity?> GetAsync(TKey key, CancellationToken cancellationToken = default);

/// <summary>Lists all tracked entities.</summary>
ValueTask<IReadOnlyList<TEntity>> ListAsync(CancellationToken cancellationToken = default);

/// <summary>Lists entities matching a specification.</summary>
ValueTask<IReadOnlyList<TEntity>> FindAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default);

/// <summary>Replaces an existing entity and rejects missing keys.</summary>
ValueTask<RepositoryResult<TEntity>> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);

/// <summary>Removes an entity by key.</summary>
ValueTask<bool> RemoveAsync(TKey key, CancellationToken cancellationToken = default);
}

/// <summary>In-memory repository implementation for tests, samples, and embedded applications.</summary>
public sealed class InMemoryRepository<TEntity, TKey> : IRepository<TEntity, TKey>
where TKey : notnull
{
private readonly Dictionary<TKey, TEntity> _entities;
private readonly Func<TEntity, TKey> _keySelector;

private InMemoryRepository(Func<TEntity, TKey> keySelector, IEqualityComparer<TKey>? comparer)
{
_keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
_entities = new Dictionary<TKey, TEntity>(comparer);
}
Comment on lines +30 to +41

/// <summary>Creates an in-memory repository builder.</summary>
public static Builder Create(Func<TEntity, TKey> keySelector)
=> new(keySelector);

/// <inheritdoc />
public ValueTask<RepositoryResult<TEntity>> AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
if (entity is null)
throw new ArgumentNullException(nameof(entity));

cancellationToken.ThrowIfCancellationRequested();
var key = _keySelector(entity);
if (_entities.ContainsKey(key))
return new ValueTask<RepositoryResult<TEntity>>(RepositoryResult<TEntity>.Conflict(entity, $"Entity with key '{key}' already exists."));

_entities[key] = entity;
return new ValueTask<RepositoryResult<TEntity>>(RepositoryResult<TEntity>.Stored(entity));
}

/// <inheritdoc />
public ValueTask<TEntity?> GetAsync(TKey key, CancellationToken cancellationToken = default)
{
if (key is null)
throw new ArgumentNullException(nameof(key));

cancellationToken.ThrowIfCancellationRequested();
_entities.TryGetValue(key, out var entity);
return new ValueTask<TEntity?>(entity);
}

/// <inheritdoc />
public ValueTask<IReadOnlyList<TEntity>> ListAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<IReadOnlyList<TEntity>>(_entities.Values.ToArray());
}

/// <inheritdoc />
public ValueTask<IReadOnlyList<TEntity>> FindAsync(ISpecification<TEntity> specification, CancellationToken cancellationToken = default)
{
if (specification is null)
throw new ArgumentNullException(nameof(specification));

cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<IReadOnlyList<TEntity>>(_entities.Values.Where(specification.IsSatisfiedBy).ToArray());
}

/// <inheritdoc />
public ValueTask<RepositoryResult<TEntity>> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
if (entity is null)
throw new ArgumentNullException(nameof(entity));

cancellationToken.ThrowIfCancellationRequested();
var key = _keySelector(entity);
if (!_entities.ContainsKey(key))
return new ValueTask<RepositoryResult<TEntity>>(RepositoryResult<TEntity>.Missing(entity, $"Entity with key '{key}' was not found."));

_entities[key] = entity;
return new ValueTask<RepositoryResult<TEntity>>(RepositoryResult<TEntity>.Stored(entity));
}

/// <inheritdoc />
public ValueTask<bool> RemoveAsync(TKey key, CancellationToken cancellationToken = default)
{
if (key is null)
throw new ArgumentNullException(nameof(key));

cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<bool>(_entities.Remove(key));
}

/// <summary>Fluent builder for in-memory repositories.</summary>
public sealed class Builder
{
private readonly Func<TEntity, TKey> _keySelector;
private IEqualityComparer<TKey>? _comparer;

internal Builder(Func<TEntity, TKey> keySelector)
{
_keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
}

/// <summary>Uses a custom key comparer.</summary>
public Builder UseComparer(IEqualityComparer<TKey> comparer)
{
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
return this;
}

/// <summary>Builds the repository.</summary>
public InMemoryRepository<TEntity, TKey> Build()
=> new(_keySelector, _comparer);
}
}

/// <summary>Result returned by repository mutation operations.</summary>
public sealed class RepositoryResult<TEntity>
{
private RepositoryResult(TEntity entity, RepositoryStatus status, string? reason)
{
Entity = entity;
Status = status;
Reason = reason;
}

/// <summary>The entity supplied to the repository operation.</summary>
public TEntity Entity { get; }

/// <summary>Operation status.</summary>
public RepositoryStatus Status { get; }

/// <summary>Gets whether the entity was stored.</summary>
public bool Succeeded => Status == RepositoryStatus.Stored;

/// <summary>Conflict or missing reason when the operation did not store the entity.</summary>
public string? Reason { get; }

/// <summary>Creates a successful mutation result.</summary>
public static RepositoryResult<TEntity> Stored(TEntity entity)
=> new(entity, RepositoryStatus.Stored, null);

/// <summary>Creates a duplicate-key result.</summary>
public static RepositoryResult<TEntity> Conflict(TEntity entity, string reason)
=> new(entity, RepositoryStatus.Conflict, ValidateReason(reason));

/// <summary>Creates a missing-entity result.</summary>
public static RepositoryResult<TEntity> Missing(TEntity entity, string reason)
=> new(entity, RepositoryStatus.Missing, ValidateReason(reason));

private static string ValidateReason(string reason)
=> string.IsNullOrWhiteSpace(reason)
? throw new ArgumentException("Repository result reason is required.", nameof(reason))
: reason;
}

/// <summary>Repository mutation status.</summary>
public enum RepositoryStatus
{
Stored,
Conflict,
Missing
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
using PatternKit.Examples.PrototypeDemo;
using PatternKit.Examples.ProxyDemo;
using PatternKit.Examples.RateLimitingDemo;
using PatternKit.Examples.RepositoryDemo;
using PatternKit.Examples.RetryDemo;
using PatternKit.Examples.Singleton;
using PatternKit.Examples.SpecificationDemo;
Expand Down Expand Up @@ -120,6 +121,7 @@ public sealed record ResilientCheckoutMailboxesExample(Func<CheckoutRequest, Che
public sealed record MessagingBackplaneFacadeExample(Func<CancellationToken, ValueTask<BackplaneDemoSummary>> RunAsync);
public sealed record GeneratedInterpreterRulesExample(Interpreter<InterpreterRulesDemo.PricingContext, decimal> Pricing, Interpreter<InterpreterRulesDemo.PricingContext, bool> Eligibility);
public sealed record LoanApprovalSpecificationsExample(SpecificationRegistry<LoanApprovalSpecificationDemo.LoanApplication> Registry, LoanApprovalService Service);
public sealed record OrderRepositoryPatternExample(OrderRepositoryDemoRunner Runner, OrderRepositoryWorkflow Workflow);
public sealed record PrototypeGameCharacterFactoryExample(Prototype<string, PrototypeDemo.PrototypeDemo.GameCharacter> Factory);
public sealed record ProxyPatternDemonstrationsExample(Proxy<int, string> RemoteProxy, Proxy<(string To, string Subject, string Body), bool> EmailProxy);
public sealed record FlyweightGlyphCacheExample(Func<string, IReadOnlyList<(FlyweightDemo.FlyweightDemo.Glyph Glyph, int X)>> RenderSentence);
Expand Down Expand Up @@ -176,6 +178,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddMessagingBackplaneFacadeExample()
.AddGeneratedInterpreterRulesExample()
.AddLoanApprovalSpecificationsExample()
.AddOrderRepositoryPatternExample()
.AddPrototypeGameCharacterFactoryExample()
.AddProxyPatternDemonstrationsExample()
.AddFlyweightGlyphCacheExample()
Expand Down Expand Up @@ -508,6 +511,15 @@ public static IServiceCollection AddLoanApprovalSpecificationsExample(this IServ
return services.RegisterExample<LoanApprovalSpecificationsExample>("Loan Approval Specifications", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddOrderRepositoryPatternExample(this IServiceCollection services)
{
services.AddOrderRepositoryDemo();
services.AddSingleton<OrderRepositoryPatternExample>(sp => new(
sp.GetRequiredService<OrderRepositoryDemoRunner>(),
sp.GetRequiredService<OrderRepositoryWorkflow>()));
return services.RegisterExample<OrderRepositoryPatternExample>("Order Repository Pattern", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddPrototypeGameCharacterFactoryExample(this IServiceCollection services)
{
services.AddSingleton(_ => PrototypeDemo.PrototypeDemo.CreateCharacterFactory());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection,
["Specification"],
["composable business rules", "source-generated registry", "DI composition"]),
Descriptor(
"Order Repository Pattern",
"src/PatternKit.Examples/RepositoryDemo/OrderRepositoryDemo.cs",
"test/PatternKit.Examples.Tests/RepositoryDemo/OrderRepositoryDemoTests.cs",
"docs/examples/order-repository-pattern.md",
ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost,
["Repository"],
["collection-like persistence boundary", "source-generated repository factory", "DI composition"]),
Descriptor(
"Generated Mailbox",
"src/PatternKit.Examples/Messaging/MailboxExample.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,19 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"test/PatternKit.Examples.Tests/SpecificationDemo/LoanApprovalSpecificationDemoTests.cs",
["fluent specification composition", "generated specification registry", "DI-importable loan approval example"]),

Pattern("Repository", PatternFamily.ApplicationArchitecture,
"docs/patterns/application/repository.md",
"src/PatternKit.Core/Application/Repository/Repository.cs",
"test/PatternKit.Tests/Application/Repository/RepositoryTests.cs",
"docs/generators/repository.md",
"src/PatternKit.Generators/Repository/RepositoryGenerator.cs",
"test/PatternKit.Generators.Tests/RepositoryGeneratorTests.cs",
null,
"docs/examples/order-repository-pattern.md",
"src/PatternKit.Examples/RepositoryDemo/OrderRepositoryDemo.cs",
"test/PatternKit.Examples.Tests/RepositoryDemo/OrderRepositoryDemoTests.cs",
["fluent async repository", "generated repository factory", "DI-importable order persistence example"]),

Pattern("Anti-Corruption Layer", PatternFamily.ApplicationArchitecture,
"docs/patterns/application/anti-corruption-layer.md",
"src/PatternKit.Core/Application/AntiCorruption/AntiCorruptionLayer.cs",
Expand Down
Loading
Loading