Effects Composition¶
The Data Store generator produces an optional Eff<RT, T> composition layer on top of the plain .NET store interfaces. This enables functional effect pipelines with built-in OpenTelemetry tracing.
Capability Interface¶
The generator produces a single capability interface per data store:
public interface IHasAppStore
{
IArticleStore ArticleStore { get; }
ICommentStore CommentStore { get; }
// ... one property per stored entity
}
Use this as a runtime constraint on effect methods — any runtime type implementing IHasAppStore can resolve stores at execution time.
Effect Methods¶
Effect methods are generated as nested static classes on the data store container:
public static partial class AppStore
{
public static partial class Articles
{
// Core CRUD
public static Eff<RT, Option<Article>> GetById<RT>(ArticleId id)
where RT : IHasAppStore => ...
public static Eff<RT, Unit> Save<RT>(Article article)
where RT : IHasAppStore => ...
public static Eff<RT, Unit> Delete<RT>(ArticleId id)
where RT : IHasAppStore => ...
// Composable queries
public static Eff<RT, QueryResult<Article>> QueryPage<RT>(
int page, int pageSize,
Expression<Func<Article, bool>>? filter = null,
Expression<Func<Article, object>>? orderBy = null)
where RT : IHasAppStore => ...
public static Eff<RT, CursorResult<Article>> QueryCursor<RT>(
string? cursor, int limit,
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
public static Eff<RT, IReadOnlyList<Article>> QueryList<RT>(
Expression<Func<Article, bool>>? filter = null,
Expression<Func<Article, object>>? orderBy = null)
where RT : IHasAppStore => ...
public static Eff<RT, Option<Article>> QueryFirst<RT>(
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
public static Eff<RT, int> QueryCount<RT>(
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
// Batch operations
public static Eff<RT, Unit> SaveMany<RT>(IEnumerable<Article> articles)
where RT : IHasAppStore => ...
public static Eff<RT, IReadOnlyList<Article>> GetByIds<RT>(IEnumerable<ArticleId> ids)
where RT : IHasAppStore => ...
public static Eff<RT, Unit> DeleteMany<RT>(IEnumerable<ArticleId> ids)
where RT : IHasAppStore => ...
// Convenience queries
public static Eff<RT, bool> Exists<RT>(ArticleId id)
where RT : IHasAppStore => ...
public static Eff<RT, int> Count<RT>()
where RT : IHasAppStore => ...
public static Eff<RT, IReadOnlyList<Article>> GetAll<RT>()
where RT : IHasAppStore => ...
// Lookup query methods (one set per [Lookup] property)
public static Eff<RT, QueryResult<Article>> QueryPageByAuthorId<RT>(
AuthorId authorId, int page, int pageSize,
Expression<Func<Article, bool>>? filter = null,
Expression<Func<Article, object>>? orderBy = null)
where RT : IHasAppStore => ...
public static Eff<RT, CursorResult<Article>> QueryCursorByAuthorId<RT>(
AuthorId authorId, string? cursor, int limit,
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
public static Eff<RT, IReadOnlyList<Article>> QueryListByAuthorId<RT>(
AuthorId authorId,
Expression<Func<Article, bool>>? filter = null,
Expression<Func<Article, object>>? orderBy = null)
where RT : IHasAppStore => ...
public static Eff<RT, Option<Article>> QueryFirstByAuthorId<RT>(
AuthorId authorId,
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
public static Eff<RT, int> QueryCountByAuthorId<RT>(
AuthorId authorId,
Expression<Func<Article, bool>>? filter = null)
where RT : IHasAppStore => ...
// Require overloads (get or fail)
public static Eff<RT, Article> Require<RT>(ArticleId id)
where RT : IHasAppStore => ...
public static Eff<RT, Article> Require<RT>(ArticleId id, Func<ArticleId, Error> onNotFound)
where RT : IHasAppStore => ...
public static Eff<RT, Article> Require<RT>(ArticleId id, Func<ArticleId, string> onNotFound)
where RT : IHasAppStore => ...
}
}
Each method lifts the corresponding IArticleStore async call into an Eff<RT, T> value.
Require — Get or Fail¶
The Require methods are convenience overloads that return Eff<RT, T> (not Option<T>). If the entity is not found, the effect fails with an error instead of returning None.
Three overloads are generated:
// Default error message: "Article {id} not found"
from article in AppStore.Articles.Require<AppRuntime>(articleId)
// Custom Error via factory
from article in AppStore.Articles.Require<AppRuntime>(articleId,
id => Error.New(404, $"No article with ID {id}"))
// Custom string message via factory
from article in AppStore.Articles.Require<AppRuntime>(articleId,
id => $"Article {id} does not exist")
Require vs GetById + guard
Use Require to replace the common GetById → guard(option.IsSome, ...) pattern with a single call. The default overload produces a clear error message including the entity name and key value(s).
OpenTelemetry Tracing¶
DataStore vs EffectsModule
DataStore generates its own effect methods with built-in OpenTelemetry instrumentation. Each effect method includes .WithActivity() calls that create tracing spans automatically. You don't need to wrap with [EffectsModule] for tracing — it's included by default. Set Instrumented = false on [DataStore] to disable it.
When Instrumented = true (the default), every effect method creates an OpenTelemetry activity span. This gives you distributed tracing out of the box without any additional configuration.
Runtime Integration¶
The [DataStore] capability is auto-discovered by the runtime in the same assembly:
[Runtime]
public sealed partial class AppRuntime;
// BlogStore is auto-discovered — no [Uses] needed
The runtime resolves all store interfaces from the DI container, making all BlogStore.* effect methods available in your pipelines.
Effect Pipeline Examples¶
Use LINQ from/select syntax to compose store operations:
// Typed IDs
[TypedId] public readonly partial struct ArticleId;
[TypedId] public readonly partial struct CommentId;
// Entities
[StoredEntity]
public partial record Article(
ArticleId Id,
string Title,
string Body,
DateTimeOffset CreatedAt);
[StoredEntity]
public partial record Comment(
CommentId Id,
[ForeignKey<Article>] ArticleId ArticleId,
string Body,
DateTimeOffset CreatedAt);
// Data store
[DataStore]
public static partial class BlogStore;
// Effect pipeline
var program =
from article in BlogStore.Articles.GetById<AppRuntime>(articleId)
from _ in guard(article.IsSome, Error.New("Article not found"))
>> BlogStore.Articles.Save<AppRuntime>(article.Value! with { Title = "Updated" })
select unit;
Adding Resilience¶
DataStore generates effect methods directly from [StoredEntity] declarations with built-in OpenTelemetry tracing. Since the store interfaces (IArticleStore, etc.) are generated, you can't add [Retry], [Timeout], or [CircuitBreaker] attributes to them directly.
To add declarative resilience, extend the generated interface and redeclare only the methods that need resilience:
public interface IResilientArticleStore : IArticleStore
{
[Retry(MaxAttempts = 3, DelayMs = 500)]
[Timeout(5_000)]
new Task<Option<Article>> GetByIdAsync(ArticleId id);
[CircuitBreaker(FailureThreshold = 5)]
[Retry]
new Task SaveAsync(Article article);
// DeleteAsync, QueryPage, QueryCursor — inherited from IArticleStore unchanged
}
[EffectsModule(typeof(IResilientArticleStore))]
public sealed partial class ResilientArticleEffects;
The generator picks up all methods — redeclared methods get resilience wrapping, inherited methods get plain liftEff + tracing. Use the resilient module's effect methods in your pipelines:
from article in ResilientArticleEffects.ArticleStore.GetById<AppRuntime>(articleId)
from _ in guard(article.IsSome, Error.New("Article not found"))
select article.Value!;
Wire both the data store (for DI registration) and the resilient module into your runtime:
[Runtime]
public sealed partial class AppRuntime;
// BlogStore and ResilientArticleEffects are auto-discovered
See Resilience for the full reference on [Retry], [Timeout], and [CircuitBreaker] attributes.
Postgres Implementation¶
The Deepstaging.Data.Postgres package includes a generator that produces EF Core implementations:
This generates:
PgArticleStore<TContext>— generic EF Core implementation ofIArticleStoreAppStoreDbContext—DbContextwithDbSet<Article>properties (for standalone use)AppStoreModelBuilderExtensions—ModelBuilderextensions for applying entity config to anyDbContextAppStorePostgresRegistration— DI extensions with default and BYOC overloads
// Standalone — uses the generated DbContext
services.AddAppStorePostgres(connectionString);
// Bring your own DbContext
services.AddDbContext<MyAppDbContext>(options => options.UseNpgsql(connectionString));
services.AddAppStorePostgres<MyAppDbContext>();
When using your own DbContext, call the generated configuration extensions in OnModelCreating and ConfigureConventions. See Generated Code → Bring Your Own DbContext for the full pattern.
The Postgres stores are registered with AddSingleton, so they take priority over the in-memory defaults registered by TryAddSingleton. See Generated Code → Swapping Implementations for the registration order pattern.