Foundatio Documentation

repository·main·Indexed 22 days ago

https://github.com/foundatiofx/foundatio

A collection of pluggable foundation blocks for building loosely coupled, distributed applications. Foundatio provides interface-first abstractions for caching (ICacheClient), queuing (IQueue<T>), messaging, storage (IFileStorage), and distributed locking (ILockProvider), allowing developers to swap implementations between in-memory, Redis, or Azure providers.

Tokens
85.4K
Snippets
217
Records
283
Agent score
84%

What's inside Foundatio

  1. What is Foundatio?

    main
    Foundatio is a modular .NET library that provides pluggable building blocks for distributed applications. It offers abstracted implementations for common infrastructure needs such as caching, queuing, locking, messaging, jobs, file storage, and resilience. This allows developers to write code against clean interfaces that can be swapped between in-memory implementations (for development and testing) and distributed implementations (for production) without changing application logic.
  2. Overview of Foundatio Core Building Blocks

    main

    Foundatio is a collection of pluggable foundation blocks designed for building loosely coupled distributed applications. Its core philosophy relies on:

    • Abstract Interfaces: Build against interfaces so implementations (In-Memory vs. Production) can be swapped easily.
    • Dependency Injection: All blocks are designed to work seamlessly with Microsoft.Extensions.DependencyInjection.
    • Local Development: Provides In-Memory implementations for all major blocks to allow development without external dependencies.
    • Extensibility: Modular design allows for custom implementations of any abstraction.
  3. Compare Foundatio ResiliencePolicy vs Polly performance

    main

    Foundatio's IResiliencePolicy is benchmarked against Polly's ResiliencePipeline. Foundatio is optimized for high-frequency operations and low-latency requirements, particularly when retries are involved.

    Key Performance Advantages

    • Synchronous Execution: Foundatio is significantly faster when retries or results are involved (up to 5.3x faster than Polly). Foundatio maintains 0 bytes of allocation for all synchronous scenarios.
    • Asynchronous Execution: Foundatio is 3.0x to 3.8x faster than Polly when retries or results are involved. Both libraries incur similar async state machine overhead (64 B).
    • Zero-Allocation Patterns: Foundatio's state-based overloads achieve true zero-allocation execution. In contrast, Polly's state-based pattern still allocates approximately 88 bytes.

    Summary Table: Foundatio Advantage

    ScenarioFoundatio Advantage
    Sync: No retryPolly is ~17% faster
    Sync: With retry5.3x faster
    Sync: With result5.1x faster
    Async: No retry~equal
    Async: With retry3.8x faster
    Async: With result3.0x faster
    Zero-Alloc: Static lambda4.6x faster
    Zero-Alloc: State-based4.3x faster (zero allocations)
  4. Implement delayed message delivery with RabbitMQ

    main

    You can schedule message delivery using the DeliveryDelay property on publish options. The behavior of delayed delivery depends on your RabbitMQ version and installed plugins:

    1. RabbitMQ < 4.3 with rabbitmq_delayed_message_exchange plugin: Foundatio uses the plugin (note: this will trigger a deprecation warning as the plugin is archived and incompatible with RabbitMQ 4.3+).
    2. RabbitMQ < 4.3 without the plugin: Foundatio falls back to an in-memory scheduler in MessageBusBase. Warning: This fallback is not durable across process restarts.
    3. RabbitMQ 4.3 and later: The delayed-exchange probe is skipped due to incompatibility, and the in-memory fallback is used automatically. Warning: This fallback is not durable across process restarts.

    For durable delayed delivery on modern RabbitMQ brokers, consider using TTL + dead-letter exchanges or an external scheduler.

  5. How RedisHybridCacheClient works (L1/L2 Caching)

    main

    The RedisHybridCacheClient implements a two-layer caching architecture to optimize performance:

    1. L1 Cache (Local In-Memory): Provides extremely low latency for frequent reads.
    2. L2 Cache (Redis Distributed): Acts as the source of truth for all instances.

    Read Flow

    Request $\rightarrow$ Check L1. If Hit: Return. If Miss: Check L2. If L2 Hit: Store in L1 and Return.

    Write Flow (Distributed-First)

    Write to L2 (Redis) first. If successful, update L1 locally and publish an invalidation message via Redis pub/sub. Other instances receive this message and clear only the specific affected keys from their local L1 caches.

    Key Features

    • Automatic Sync: Uses Redis pub/sub to keep L1 caches consistent across instances.
    • Prefix Removal: RemoveByPrefixAsync("user:") clears all user:* keys across all instances.
    • Full Flush: RemoveAllAsync() clears the entire L1 cache on all instances.
    WARNING

    By default, all instances share the same Redis pub/sub topic for invalidation. In high-write scenarios, consider using separate topics per feature area.

    var hybridCache = new RedisHybridCacheClient(
        redisConfig => redisConfig.ConnectionMultiplexer(redis).LoggerFactory(loggerFactory),
        localConfig => localConfig.MaxItems(1000)
    );
    
    // When you update a value, all instances are notified via pub/sub
    await hybridCache.SetAsync("config:app", newConfig);
  6. How poison messages are handled in queues

    main

    When a message cannot be deserialized during dequeue (e.g., due to corrupted data or schema changes), Foundatio handles it as follows:

    1. The deserialization exception is caught and logged.
    2. The message is abandoned via AbandonAsync, which increments the attempt counter and applies retry delay/backoff.
    3. null is returned from DequeueAsync, so the consumer does not see the corrupted message.
    4. Once the retry limit is exhausted, the message is moved to the dead letter queue.

    This allows operators to fix configuration issues (like a missing JsonConverter) and have the message eventually succeed if the fix is deployed before retries are exhausted.

  7. Distinguish between Queue Name and Queue ID

    main

    Foundatio queues use two different identifiers that serve distinct roles. Understanding the difference is critical for data routing and cross-process communication.

    Queue Name

    • Purpose: Identifies the queue in the backing store (e.g., Redis key prefix, SQS queue name).
    • Stability: Stable across restarts.
    • Sharing: Shared across processes. Two processes with the same Name will read from and write to the same data.
    • Routing: All distributed implementations (Redis, SQS, Azure Service Bus, Azure Storage) use Name for data routing.

    Queue ID

    • Purpose: A runtime instance identifier used exclusively for logging and diagnostics.
    • Stability: Not stable (uses a random suffix by default).
    • Sharing: Not shared across processes; it has no effect on data routing or the backing store.
    • Use Case: Allows multiple queue instances within the same process (like priority queues) to produce distinguishable logs.
  8. Best practices for implementing Foundatio jobs

    main

    To ensure reliability and correctness, follow these patterns when implementing jobs:

    1. Propagate Cancellation Tokens: Always pass context.CancellationToken to async calls and check it in loops to ensure graceful shutdown.
    2. Renew Locks: For long-running jobs using distributed locks, call context.RenewLockAsync() periodically to prevent the lock from expiring mid-run.
    3. Ensure Idempotency: Jobs may be interrupted (crashes, deployments). Design them to track progress (e.g., via external state) so they can resume without re-processing completed work.
    4. Handle Errors Correctly:
      • Return JobResult.FailedWithMessage(msg) for transient errors to trigger framework retries.
      • Return JobResult.Success for permanent errors (after logging) to prevent infinite retry loops.
    5. Use Structured Logging: Use _logger.BeginScope to correlate log entries with specific work items (e.g., OrderId).
  9. Compare JobWithLockBase and manual locking in JobBase

    main

    Choosing between JobWithLockBase and manual locking depends on the required granularity:

    • Use JobWithLockBase when the entire run must be single-instance. The framework manages the lock lifecycle (acquisition and release) automatically. Set timeUntilExpires to at least 2-3x your expected run duration to allow for self-healing after a crash.
    • Use manual ILockProvider.AcquireAsync inside JobBase when you need fine-grained control, such as locking individual resources (e.g., specific database rows) while allowing the job itself to run concurrently on multiple servers.
  10. Understand the ISerializer and ITextSerializer interfaces

    main

    Foundatio uses an abstraction layer for serialization, allowing you to swap implementations (like JSON or binary) without changing your business logic.

    • ISerializer: The base interface providing Deserialize(Stream data, Type objectType) and Serialize(object? value, Stream output).
    • ITextSerializer: A marker interface for serializers that produce human-readable text (e.g., JSON, XML). Text serializers use UTF-8 encoding for string conversions in extension methods.

    Binary serializers (like MessagePack) implement ISerializer but not ITextSerializer. When using string-based extension methods with binary serializers, strings are treated as Base64-encoded data, whereas text serializers treat them as UTF-8.

    public interface ISerializer
    {
        object? Deserialize(Stream data, Type objectType);
        void Serialize(object? value, Stream output);
    }
    
    public interface ITextSerializer : ISerializer { }
  11. How HybridCacheClient works (Read and Write flows)

    main

    HybridCacheClient uses a two-tier (L1/L2) architecture to balance speed and consistency.

    Read Flow

    1. Check L1 (Local Cache): Fast in-process memory check. If hit, return immediately.
    2. Check L2 (Distributed Cache): On L1 miss, check the distributed cache (e.g., Redis).
    3. Backfill L1: If found in L2, the value is stored in L1 for future requests before being returned.

    Write Flow

    1. Write to L2 first: The distributed cache is the source of truth. The write must succeed here first.
    2. Update L1: Only if the L2 write succeeds is the local cache updated.
    3. Invalidate others: A message is published via the message bus. Other instances receive this and clear only the specific affected keys from their local caches. The current instance ignores its own invalidation messages via CacheId filtering.
  12. How SQSMessageBus durable subscriptions work

    main

    By default, SQSMessageBus creates temporary queues that are deleted when the subscriber is disposed. To implement Durable Subscriptions (queues that persist across service restarts), you must:

    1. Provide a specific name via SubscriptionQueueName.
    2. Set SubscriptionQueueAutoDelete to false.

    This ensures the SQS queue remains in AWS, allowing the subscriber to pick up messages that were sent while the service was offline.

    var messageBus = new SQSMessageBus(o =>
    {
        o.ConnectionString = connectionString;
        o.Topic = "events";
        o.SubscriptionQueueName = "order-service-events";
        o.SubscriptionQueueAutoDelete = false; // Queue persists across restarts
    });