Provider Behavioral Gaps
This document catalogs known behavioral differences across Foundatio provider implementations. While all providers implement the same interfaces, underlying infrastructure limitations mean some operations behave differently than the interface contract might suggest. Use this as a compatibility reference when choosing or switching providers.
Summary
| Interface | InMemory | Redis | Azure | AWS | RabbitMQ | Kafka | Aliyun | SshNet |
|---|---|---|---|---|---|---|---|---|
IFileStorage | Full | Full | Full | Partial | — | — | Full | Full |
IQueue<T> | Full | Partial | Partial | Partial | — | — | — | — |
IMessageBus | Full | Full | Full | Partial | Full | Full | — | — |
ICacheClient | Full | Full | — | — | — | — | — | — |
ILockProvider | Full | Full | — | — | — | — | — | — |
Legend: Full = all interface behaviors work as documented. Partial = some operations diverge (see below). — = not implemented by this provider.
IFileStorage
DeleteFileAsync for Non-Existent Files
| Provider | Behavior | Notes |
|---|---|---|
| InMemory | Returns false | ✅ Expected |
| Redis | Returns false | ✅ Expected |
| Azure Blob | Returns false | ✅ Expected |
| S3 | Returns true | ⚠️ S3 DELETE is idempotent by design |
| Aliyun | Returns false | ✅ Expected |
| SshNet | Returns false | ✅ Expected |
Impact: Code that uses the return value to determine whether a file existed before deletion will get incorrect results on S3.
CopyFileAsync / RenameFileAsync with Non-Existent Source
| Provider | Behavior | Notes |
|---|---|---|
| InMemory | Returns false | ✅ Expected |
| Redis | Returns false | ✅ Expected |
| Azure Blob | Returns false | ✅ Expected |
| S3 | Throws AmazonS3Exception | ⚠️ Exception instead of false |
| Aliyun | Returns false | ✅ Expected |
| SshNet | Returns false | ✅ Expected |
Impact: Callers must wrap S3 copy/rename in try-catch if the source file might not exist.
GetFileStreamAsync with Non-Existent File (ReadMode)
| Provider | Behavior | Notes |
|---|---|---|
| InMemory | Returns null | ✅ Expected |
| Redis | Returns null | ✅ Expected |
| Azure Blob | Returns null | ✅ Expected |
| S3 | Throws AmazonS3Exception | ⚠️ Exception instead of null |
| Aliyun | Returns null | ✅ Expected |
| SshNet | Returns null | ✅ Expected |
GetFileStreamAsync with StreamMode.Write
| Provider | Supported | Notes |
|---|---|---|
| InMemory | ✅ | Full support |
| Azure Blob | ✅ | Full support |
| Aliyun | ✅ | Full support |
| SshNet | ✅ | Full support |
| Redis | ❌ | Not supported |
| S3 | ❌ | Not supported |
IQueue<T>
GetDeadletterItemsAsync
| Provider | Supported | Notes |
|---|---|---|
| InMemory | ✅ | Returns all deadlettered entries |
| Redis | ❌ | Not implemented |
| Azure Service Bus | ❌ | Deadletter is a separate sub-queue; requires different API |
| Azure Storage Queue | ❌ | No deadletter concept in Azure Storage Queues |
| SQS | ❌ | Deadletter is a separate queue; not retrievable via this API |
QueueEntryOptions.UniqueId
| Provider | Supported | Notes |
|---|---|---|
| InMemory | ✅ | Uses provided ID as the entry ID |
| Redis | ✅ | Uses provided ID as the entry ID |
| Azure Service Bus | ✅ | Maps to MessageId (deduplicated only when RequiresDuplicateDetection is enabled) |
| Azure Storage Queue | ❌ | Azure assigns its own MessageId |
| SQS | ⚠️ | SQS assigns its own MessageId. On FIFO queues the value is sent as MessageDeduplicationId; on standard queues it is not sent |
QueueEntryOptions.GroupId
| Provider | Effect on delivery | Notes |
|---|---|---|
| InMemory | ❌ | Stored and returned on entry.GroupId only |
| Redis | ❌ | Stored in the payload envelope and returned on entry.GroupId only |
| Azure Storage Queue | ❌ | Stored in the payload envelope (default compatibility mode); not stored in legacy mode |
| Azure Service Bus | ⚠️ | Sent as SessionId. On session-enabled queues (RequiresSession), ordered per group, one message at a time per group; ignored otherwise |
| SQS standard queue | ✅ | Sent as MessageGroupId to enable fair queues (noisy neighbor mitigation). No ordering |
| SQS FIFO queue | ✅ | Sent as MessageGroupId. Strict ordering within a group; required by SQS |
Queues that do not use the value for delivery log a single debug message the first time a group id is enqueued. See Message Groups.
Queue Ordering Guarantees
| Provider | Ordering | Notes |
|---|---|---|
| InMemory | ✅ | FIFO within the queue |
| Redis | ✅ | FIFO list; retried entries are re-queued |
| Azure Storage Queue | ⚠️ | Best effort |
| Azure Service Bus | ⚠️ | Best effort; concurrent workers and retries can reorder processing. On session-enabled queues, ordered per GroupId; an abandoned message is retried before later messages in its group |
| SQS standard queue | ❌ | Best effort. GroupId does not change ordering |
| SQS FIFO queue | ✅ | Strict ordering within a GroupId |
Delivery Delay (DelayUntilUtc)
| Provider | Supported | Granularity | Notes |
|---|---|---|---|
| InMemory | ✅ | Millisecond | Precise delay via background timer |
| Redis | ❌ | — | Not supported |
| Azure Storage Queue | ✅ | Second | VisibilityTimeout parameter |
| Azure Service Bus | ✅ | Second | ScheduledEnqueueTimeUtc |
| SQS | ⚠️ | Second | DelaySeconds, clamped to 900s. FIFO queues reject a positive delay |
Retry Delay on Abandon
| Provider | Mechanism | Delivery | Notes |
|---|---|---|---|
| InMemory | Re-added after a timer | At-least-once | Pending retries are lost if the process exits |
| Redis | Wait list | At-least-once | Moved back by queue maintenance |
| Azure Storage Queue | Visibility timeout on the same message | At-least-once | Message ID is unchanged |
| Azure Service Bus | Scheduled copy of the message; immediate abandon on session-enabled queues or when duplicate detection is enabled | At-least-once | The copy is sent before the original is completed, so the original can be redelivered alongside the retry if completion fails. It keeps MessageId, CorrelationId, Subject, ContentType and properties. Session-enabled queues ignore RetryDelay so the retry stays at the head of its group |
| SQS | Visibility timeout on the same message | At-least-once | Message ID is unchanged. On FIFO queues, later messages in the group wait for the retry |
Handlers should be idempotent with every provider; see message loss and duplicates for the Service Bus guidance.
Service Bus delayed retries reuse the original MessageId, so on a queue with duplicate detection enabled (RequiresDuplicateDetection) the copy would be discarded as a duplicate. The queue therefore abandons immediately instead and does not apply RetryDelay when duplicate detection is enabled.
IMessageBus
Delayed Message Delivery
| Provider | Supported | Granularity | Notes |
|---|---|---|---|
| InMemory | ✅ | Millisecond | Background timer, precise |
| Redis | ✅ | Millisecond | Lua script with delay |
| Azure Service Bus | ✅ | Second | Scheduled enqueue |
| RabbitMQ | ✅ | Millisecond | Via delayed-message-exchange plugin |
| Kafka | ✅ | Millisecond | Client-side delay before produce |
| SQS/SNS | ⚠️ | Second | Impractical due to polling latency |
Note: For distributed providers, delay precision is limited by network latency and polling intervals. Tests use 1-second minimum delay to accommodate provider-level granularity.
Message Ordering Guarantees
| Provider | Ordered | Notes |
|---|---|---|
| InMemory | ✅ | FIFO within a single subscriber |
| Redis | ✅ | Pub/sub delivers in publish order |
| Azure Service Bus | ✅ | FIFO with sessions enabled |
| RabbitMQ | ✅ | FIFO per queue |
| Kafka | ✅ | FIFO per partition |
| SQS/SNS | ⚠️ | Standard queues are best-effort ordering |
ICacheClient
All tested providers (InMemory, Redis) exhibit fully consistent behavior. No behavioral gaps were found for:
AddAsync(returnsfalsewhen key exists)GetAsync(returns no-value for missing keys)RemoveAsync(returnsfalsefor non-existent keys)RemoveByPrefixAsync(returns 0 for no matches)IncrementAsync(with zero amount returns current value)SetExpirationAsync(no-op for non-existent keys)
ILockProvider
All tested providers (InMemory, Redis) exhibit fully consistent behavior. No behavioral gaps were found for:
AcquireAsyncwithreleaseOnDispose: falseReleaseAsyncwith force releaseILockmetadata properties (LockId,Resource,AcquiredTimeUtc)TryUsingAsyncexecution and automatic release
Recommendations
Don't rely on
DeleteFileAsyncreturn value for existence checks on S3-compatible storage. UseGetFileInfoAsyncfirst if you need to know whether a file existed.Wrap S3 copy/rename/read operations in try-catch if the source file might not exist. Other providers return
false/nullgracefully.Use at least 1-second granularity for delivery delays when targeting distributed providers. Sub-second delays are only reliable with InMemory.
Don't rely on
GetDeadletterItemsAsync— only InMemory supports it. Design deadletter processing around provider-specific mechanisms instead.Prefer
QueueEntryOptions.UniqueIdonly with providers that support it (InMemory, Redis, Azure Service Bus). On SQS and Azure Storage Queues, the system assigns its own IDs.Treat
QueueEntryOptions.GroupIdas a hint, not a guarantee. It only changes delivery on SQS (fair queues on standard queues, ordering on FIFO) and session-enabled Service Bus queues (ordering per group); the other built-in queues store it as metadata.