Skip to content

Effects Composition

The IdempotencyModule is an [EffectsModule(typeof(IIdempotencyStore))] that generates Eff<RT, T> effect methods for every IIdempotencyStore method.

Capability Interface

public interface IHasIdempotencyStore
{
    IIdempotencyStore IdempotencyStore { get; }
}

Effect Methods

public static partial class IdempotencyModule
{
    public static partial class Idempotency
    {
        public static Eff<RT, bool> TryClaimAsync<RT>(string key, TimeSpan? expiry = null)
            where RT : IHasIdempotencyStore => ...

        public static Eff<RT, bool> IsClaimedAsync<RT>(string key)
            where RT : IHasIdempotencyStore => ...

        public static Eff<RT, Unit> ReleaseAsync<RT>(string key)
            where RT : IHasIdempotencyStore => ...

        public static Eff<RT, CachedResponse?> TryGetCachedResponseAsync<RT>(string key)
            where RT : IHasIdempotencyStore => ...

        public static Eff<RT, Unit> StoreCachedResponseAsync<RT>(
            string key, CachedResponse response, TimeSpan? expiry = null)
            where RT : IHasIdempotencyStore => ...
    }
}

Effect Pipeline Examples

Manual idempotency in an event handler

// Deduplicate webhook processing using the event's unique ID
var processWebhook =
    from claimed in IdempotencyModule.Idempotency.TryClaimAsync<AppRuntime>(
        $"webhook:{webhook.EventId}", TimeSpan.FromHours(24))
    from _ in claimed
        ? ProcessEvent(webhook)
        : unitEff  // Already processed — skip silently
    select unit;

Claim with error recovery

// If processing fails, release the key so it can be retried
var processWithRecovery =
    from claimed in IdempotencyModule.Idempotency.TryClaimAsync<AppRuntime>(
        key, TimeSpan.FromHours(1))
    from _ in Guard<AppRuntime>(claimed, Error.New(409, "Already processing"))
    from result in ProcessPayment(command)
        | @catch(e =>
            from release in IdempotencyModule.Idempotency.ReleaseAsync<AppRuntime>(key)
            from fail in FailEff<AppRuntime, PaymentResult>(e)
            select fail)
    select result;

Compose with dispatch

// Check idempotency before executing a command
var idempotentCommand =
    from claimed in IdempotencyModule.Idempotency.TryClaimAsync<AppRuntime>(
        $"cmd:{cmd.RequestId}")
    from result in claimed
        ? AppDispatch.Dispatch(cmd)
        : from cached in IdempotencyModule.Idempotency.TryGetCachedResponseAsync<AppRuntime>(
              $"cmd:{cmd.RequestId}")
          select cached  // Return previously cached result
    select result;

Most idempotency is handled automatically by the middleware — use these effect methods for manual control in event handlers, background jobs, or integration event subscribers.