Metran 3.0.0
dotnet add package Metran --version 3.0.0
NuGet\Install-Package Metran -Version 3.0.0
<PackageReference Include="Metran" Version="3.0.0" />
<PackageVersion Include="Metran" Version="3.0.0" />
<PackageReference Include="Metran" />
paket add Metran --version 3.0.0
#r "nuget: Metran, 3.0.0"
#:package Metran@3.0.0
#addin nuget:?package=Metran&version=3.0.0
#tool nuget:?package=Metran&version=3.0.0
Metran
Thread-safe, in-memory transaction manager.
Latest Version
v3.0 (Breaking Changes)
What it does?
- Metran provides a thread-safe transaction manager that wraps
ConcurrentDictionaryfor easy in-memory transaction management. - Its primary use case is to prevent concurrent execution of a specific operation by a single user or a group of users within the same application instance. For example, blocking a user from calling the same endpoint multiple times simultaneously.
- This project's goal is to offer a straightforward wrapper around
ConcurrentDictionaryto manage processing IDs in memory. - Important: Metran is designed for single-instance applications and does not support distributed systems.
Breaking Changes in v3.0
- Removed
ForceAddTransaction(T transactionIdentity): This method, which previously allowed overwriting an existing transaction, has been removed to align with the core principle of managing unique, non-overlapping locks. - Renamed
ThrowOrAddTransactiontoAddTransaction: This method is now the standard way to explicitly add a single transaction, throwing anInvalidOperationExceptionif a transaction with the same ID already exists. - Renamed
ForceAddTransactionListtoAddTransactionList: Similar toAddTransaction, this method is now the standard way to atomically add a batch of transactions, throwing anInvalidOperationExceptionif any of the provided IDs are already in use.
How to use?
You can check the example project for detailed usage.
1. Define MetranContainer
Start by defining a MetranContainer instance, typically as a static field for application-wide access:
public static readonly MetranContainer<long> Container = new();
2. Basic Transaction Usage (Single ID)
Use the MetranContainer within your methods to manage transactions. When the using block exits, the transaction is automatically disposed, releasing the ID.
public void DoSomething(long userId)
{
// AddTransaction: Attempts to add a new transaction for userId.
// Throws InvalidOperationException if userId is already associated with an active transaction.
using var metran = Container.AddTransaction(userId);
// Do your business logic here.
// While 'metran' is in scope, 'userId' is considered "in transaction" and locked.
}
Alternative Transaction Creation Methods:
GetOrAddTransaction(T transactionIdentity): Returns an existing transaction if found. If no transaction with the giventransactionIdentityexists, it atomically adds and returns a new one. This method is idempotent and thread-safe, making it suitable for scenarios where you want to proceed with an existing transaction or create a new one if it's free.TryAddTransaction(T transactionIdentity, out MetranTransaction<T> transaction): Attempts to add a new transaction. Returnstrueif successful (i.e., the transaction was added because it didn't exist) and setstransaction,falseotherwise (if a transaction with the same ID already exists). This method is non-blocking and safe for concurrent attempts to acquire a lock, allowing for custom handling of conflicts without exceptions.
3. Transaction Usage with a List of IDs
For operations involving multiple IDs that need to be locked atomically, use the AddTransactionList or TryAddTransactionList methods:
public void DoSomethingWithMultipleUsers(List<long> userIds)
{
// AddTransactionList: Attempts to add transactions for all provided IDs as a single atomic unit.
// If ANY of the IDs in the HashSet already exist as active transactions,
// it will throw an InvalidOperationException, and none of the transactions will be added.
using var metranList = Container.AddTransactionList(userIds.ToHashSet());
// Do your business logic here.
// While 'metranList' is in scope, all 'userIds' are considered "in transaction" and locked.
}
Alternative Transaction List Creation Method:
TryAddTransactionList(HashSet<T> transactionIdentityList, out MetranTransactionList<T> transactionList): Attempts to add transactions for all provided IDs atomically. Returnstrueif all are successfully added (meaning none of them existed previously) and setstransactionList. Returnsfalseotherwise (if even one ID already exists or the list is empty). Iffalseis returned, no transactions are ultimately held, and any partially added transactions are automatically disposed to maintain atomicity. This method is non-blocking and ideal for concurrent lock acquisition where you want to handle conflicts gracefully.
4. Checking for Existing Transactions
public bool IsUserProcessing(long userId)
{
// HasTransaction: Checks if a transaction with the given userId is currently active.
return MetranContainer.Container.HasTransaction(userId);
}
5. Waiting for Transactions to Complete
Metran provides asynchronous waiting capabilities for individual transactions and lists of transactions. This is useful when you need to wait for an ongoing operation (identified by a transaction) to complete before proceeding.
Waiting for a Single Transaction
public async Task WaitForUserOperation(long userId)
{
MetranTransaction<long> transaction;
// Attempt to add a transaction. If it already exists, 'TryAddTransaction' will return false,
// and we can then choose to wait for the existing one.
if (Container.TryAddTransaction(userId, out transaction))
{
// This transaction was just added, meaning no one else is currently processing for this user.
// Perform your operation, then dispose the transaction using a 'using' statement.
using (transaction)
{
Console.WriteLine($"Starting operation for user {userId}...");
await Task.Delay(2000); // Simulate work
Console.WriteLine($"Operation for user {userId} completed.");
}
}
else
{
// A transaction for this userId already exists, meaning another operation is ongoing.
// Get the existing transaction to wait for it.
transaction = Container.GetOrAddTransaction(userId);
try
{
Console.WriteLine($"User {userId} is busy. Waiting for existing operation to complete...");
// Wait for the existing transaction to complete (be disposed).
// This will throw a TimeoutException if it doesn't complete within 10 seconds.
await transaction.WaitAsync();
Console.WriteLine($"Existing operation for user {userId} completed. You can now proceed.");
}
catch (TimeoutException ex)
{
Console.WriteLine($"Waiting for operation for user {userId} timed out: {ex.Message}");
}
catch (OperationCanceledException)
{
Console.WriteLine($"Waiting for user {userId} operation was cancelled.");
}
}
}
WaitAsync and SafeWaitAsync parameters:
waitDelayMiliseconds(default: 100): The delay in milliseconds between checks for the transaction's completion.timeoutMiliseconds(default: 10000): The maximum time in milliseconds to wait before throwing aTimeoutException(forWaitAsync) or returningfalse(forSafeWaitAsync).cancellationToken(default:CancellationToken.None): ACancellationTokento cancel the wait operation.
Waiting for a List of Transactions
When dealing with MetranTransactionList, the typical pattern is to acquire all locks atomically using TryAddTransactionList or AddTransactionList. If TryAddTransactionList fails, it means one or more transactions are already active, and you might choose to handle the failure (e.g., return an error, retry after a delay) rather than waiting for an arbitrarily overlapping set of transactions.
The WaitAllAsync and SafeWaitAllAsync methods for MetranTransactionList are primarily intended to be used after you have successfully acquired all locks (e.g., within the using block of a MetranTransactionList) to wait for a subset of those transactions to complete, or if you explicitly know transactions for a list of IDs should be active and want to wait for them to finish.
public async Task TryAcquireAndProcessMultipleUsers(List<long> userIds)
{
var idsToProcess = userIds.ToHashSet();
MetranTransactionList<long> transactionList;
if (Container.TryAddTransactionList(idsToProcess, out transactionList))
{
// All transactions were successfully added. Process them.
using (transactionList)
{
Console.WriteLine($"Acquired locks for users: {string.Join(", ", userIds)}. Processing...");
await Task.Delay(3000); // Simulate work
Console.WriteLine($"Finished processing for users: {string.Join(", ", userIds)}. Releasing locks.");
}
}
else
{
Console.WriteLine($"Could not acquire all locks for users: {string.Join(", ", userIds)}. Some users are busy.");
// At this point, you might inform the user, log, or implement a retry mechanism.
// For example, if you wanted to wait for the *specific* IDs that were busy,
// you would need to retrieve them individually using Container.GetOrAddTransaction()
// and then call individual `SafeWaitAsync` on each, or implement a loop that retries
// `TryAddTransactionList` after a delay.
}
}
WaitAllAsync and SafeWaitAllAsync parameters:
waitDelayMiliseconds(default: 100): The delay in milliseconds between checks for the transactions' completion.timeoutMiliseconds(default: 10000): The maximum time in milliseconds to wait before throwing aTimeoutException(forWaitAllAsync) or returningfalse(forSafeWaitAllAsync).cancellationToken(default:CancellationToken.None): ACancellationTokento cancel the wait operation.
Changelog
v3.0 (Breaking Changes)
- Removed
ForceAddTransaction(T transactionIdentity): This method was removed as its behavior of overwriting existing transactions did not align with the library's goal of managing unique concurrent locks. - Renamed
ThrowOrAddTransactiontoAddTransaction: This change makesAddTransactionthe primary method for adding a single transaction, explicitly throwing anInvalidOperationExceptionif a conflict occurs. - Renamed
ForceAddTransactionListtoAddTransactionList: This change makesAddTransactionListthe primary method for atomically adding a batch of transactions, throwing anInvalidOperationExceptionif any conflict occurs within the batch. - Corrected
HasTransactionimplementation: Removed unnecessarylockkeyword, asConcurrentDictionary.ContainsKeyis already thread-safe. - Enhanced XML Documentation: Added comprehensive XML documentation to all public methods and classes for improved clarity and IntelliSense support.
- Refined internal
TryAddTransactionandAddTransactionlogic: UtilizedConcurrentDictionary.TryAddmore directly for atomic operations where applicable. - Minor
MetranTransactionList.TryAddTransactionListrefinement: Added an early exit check for null or emptytransactionIdentityList. - Added asynchronous waiting capabilities:
MetranTransaction<T>.WaitAsync(): Waits for a single transaction to complete, throwing aTimeoutExceptionon timeout.MetranTransaction<T>.SafeWaitAsync(): Waits for a single transaction to complete, returningfalseon timeout instead of throwing an exception.MetranTransactionList<T>.WaitAllAsync(): Waits for all transactions in the list to complete, throwing aTimeoutExceptionon timeout.MetranTransactionList<T>.SafeWaitAllAsync(): Waits for all transactions in the list to complete, returningfalseon timeout instead of throwing an exception.- Improved
MetranTransactionListperformance and consistency: Now usesHashSet<MetranTransaction<T>>internally for storing the list of transactions, providing efficient lookups and set operations.
v2.0
- Removed retry functions; retry logic should be handled by the consuming application.
- Deleted
BeginTransactionmethods. - Added
TryAddTransactionandTryAddTransactionListmethods for non-blocking transaction creation. - Added more methods to
MetranContainerfor greater flexibility in transaction management.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 is compatible. net5.0-windows was computed. net5.0-windows7.0 is compatible. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net6.0-windows7.0 is compatible. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net7.0-windows7.0 is compatible. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net8.0-windows7.0 is compatible. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| .NET Framework | net481 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8.1
- No dependencies.
-
.NETStandard 2.1
- No dependencies.
-
net5.0
- No dependencies.
-
net5.0-windows7.0
- No dependencies.
-
net6.0
- No dependencies.
-
net6.0-windows7.0
- No dependencies.
-
net7.0
- No dependencies.
-
net7.0-windows7.0
- No dependencies.
-
net8.0
- No dependencies.
-
net8.0-windows7.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.