Foundatio Documentation
repository·main·Indexed 22 days ago
https://github.com/foundatiofx/foundatioA 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.
What's inside Foundatio
- 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.
Overview of Foundatio Core Building Blocks
mainFoundatio 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.
Compare Foundatio ResiliencePolicy vs Polly performance
mainFoundatio's
IResiliencePolicyis benchmarked against Polly'sResiliencePipeline. 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
Scenario Foundatio Advantage Sync: No retry Polly is ~17% faster Sync: With retry 5.3x faster Sync: With result 5.1x faster Async: No retry ~equal Async: With retry 3.8x faster Async: With result 3.0x faster Zero-Alloc: Static lambda 4.6x faster Zero-Alloc: State-based 4.3x faster (zero allocations) Implement delayed message delivery with RabbitMQ
mainYou can schedule message delivery using the
DeliveryDelayproperty on publish options. The behavior of delayed delivery depends on your RabbitMQ version and installed plugins:- RabbitMQ < 4.3 with
rabbitmq_delayed_message_exchangeplugin: Foundatio uses the plugin (note: this will trigger a deprecation warning as the plugin is archived and incompatible with RabbitMQ 4.3+). - 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. - 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.
- RabbitMQ < 4.3 with
How RedisHybridCacheClient works (L1/L2 Caching)
mainThe
RedisHybridCacheClientimplements a two-layer caching architecture to optimize performance:- L1 Cache (Local In-Memory): Provides extremely low latency for frequent reads.
- 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 alluser:*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);How poison messages are handled in queues
mainWhen a message cannot be deserialized during dequeue (e.g., due to corrupted data or schema changes), Foundatio handles it as follows:
- The deserialization exception is caught and logged.
- The message is abandoned via
AbandonAsync, which increments the attempt counter and applies retry delay/backoff. nullis returned fromDequeueAsync, so the consumer does not see the corrupted message.- 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.Distinguish between Queue Name and Queue ID
mainFoundatio 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
Namewill read from and write to the same data. - Routing: All distributed implementations (Redis, SQS, Azure Service Bus, Azure Storage) use
Namefor 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.
Best practices for implementing Foundatio jobs
mainTo ensure reliability and correctness, follow these patterns when implementing jobs:
- Propagate Cancellation Tokens: Always pass
context.CancellationTokento async calls and check it in loops to ensure graceful shutdown. - Renew Locks: For long-running jobs using distributed locks, call
context.RenewLockAsync()periodically to prevent the lock from expiring mid-run. - 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.
- Handle Errors Correctly:
- Return
JobResult.FailedWithMessage(msg)for transient errors to trigger framework retries. - Return
JobResult.Successfor permanent errors (after logging) to prevent infinite retry loops.
- Return
- Use Structured Logging: Use
_logger.BeginScopeto correlate log entries with specific work items (e.g.,OrderId).
- Propagate Cancellation Tokens: Always pass
Compare JobWithLockBase and manual locking in JobBase
mainChoosing between
JobWithLockBaseand manual locking depends on the required granularity:- Use
JobWithLockBasewhen the entire run must be single-instance. The framework manages the lock lifecycle (acquisition and release) automatically. SettimeUntilExpiresto at least 2-3x your expected run duration to allow for self-healing after a crash. - Use manual
ILockProvider.AcquireAsyncinsideJobBasewhen 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.
- Use
Understand the ISerializer and ITextSerializer interfaces
mainFoundatio uses an abstraction layer for serialization, allowing you to swap implementations (like JSON or binary) without changing your business logic.
ISerializer: The base interface providingDeserialize(Stream data, Type objectType)andSerialize(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
ISerializerbut notITextSerializer. 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 { }How HybridCacheClient works (Read and Write flows)
mainHybridCacheClientuses a two-tier (L1/L2) architecture to balance speed and consistency.Read Flow
- Check L1 (Local Cache): Fast in-process memory check. If hit, return immediately.
- Check L2 (Distributed Cache): On L1 miss, check the distributed cache (e.g., Redis).
- Backfill L1: If found in L2, the value is stored in L1 for future requests before being returned.
Write Flow
- Write to L2 first: The distributed cache is the source of truth. The write must succeed here first.
- Update L1: Only if the L2 write succeeds is the local cache updated.
- 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
CacheIdfiltering.
How SQSMessageBus durable subscriptions work
mainBy default,
SQSMessageBuscreates temporary queues that are deleted when the subscriber is disposed. To implement Durable Subscriptions (queues that persist across service restarts), you must:- Provide a specific name via
SubscriptionQueueName. - Set
SubscriptionQueueAutoDeletetofalse.
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 });- Provide a specific name via