vault backup: 2025-06-23 00:01:29

This commit is contained in:
2025-06-23 00:01:29 +03:30
parent a7f5370340
commit 8e7ad789fb
1347 changed files with 0 additions and 229 deletions
@@ -0,0 +1,766 @@
To **safely handle concurrency** in your `CreateTicketOrderAsync` method — particularly for **seat reservation** on the same `Transportation` — you need to **prevent race conditions** where two users might reserve the same seat or oversell capacity.
This is a **classic critical section problem**, and you can solve it using **application-level locking**, **database-level locking**, or both.
---
## ✅ Recommended: **Database-level concurrency control + optional distributed lock**
### 👇 Heres what you should do:
---
## ✅ 1. Use **pessimistic concurrency** (row-level locking) on the `Transportation` row
### Why?
You need to **lock the transportation row** to prevent multiple reservations from modifying the related seats/capacity **at the same time**.
### How?
Use EF Core's **raw SQL or transaction-scoped SELECT ... FOR UPDATE** (which translates to SQL row locks):
```csharp
using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
var transportation = await _context.Transportations
.FromSqlRaw("SELECT * FROM Transportations WITH (UPDLOCK, ROWLOCK) WHERE Id = {0}", dto.TransportationId)
.FirstOrDefaultAsync();
```
- `UPDLOCK`: prevents others from reading for update
- `ROWLOCK`: locks only the specific row
> If using PostgreSQL or MySQL, youd use `FOR UPDATE` instead.
---
## ✅ 2. Alternatively, use **distributed locking** if you have multiple app instances
Use something like:
- **Redis distributed lock**
- **SQL application locks** (`sp_getapplock`)
- **SemaphoreSlim static lock** (only for single-instance apps — not safe in web farms)
### Example using `sp_getapplock` (SQL Server):
```sql
EXEC sp_getapplock @Resource = 'lock_transportation_123', @LockMode = 'Exclusive', @LockTimeout = 10000;
```
EF version:
```csharp
await _dbContext.Database.ExecuteSqlRawAsync(
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', @LockMode = 'Exclusive', @LockTimeout = 10000;");
```
✅ This will ensure **only one thread** gets to proceed per `TransportationId`.
---
## ✅ 3. Wrap the reservation logic in a **transaction**
```csharp
using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
// locking query here
// seat validation
// insert tickets
// save ticket order
// commit
await _unitOfWork.SaveChangesAsync();
await transaction.CommitAsync();
```
Use `IsolationLevel.Serializable` or `RepeatableRead` if youre not doing explicit locking.
---
## ✅ 4. Add a **unique constraint** on `SeatId + TransportationId` (if not already)
To **enforce uniqueness at DB level** and prevent race conditions from succeeding:
```sql
ALTER TABLE Tickets ADD CONSTRAINT UQ_Tickets_Seat_Transport UNIQUE (SeatId, TicketStatusId)
WHERE TicketStatusId != 3; -- exclude cancelled
```
Or make a logic-safe alternative depending on your schema.
---
## ✅ Final version: Summary of changes
### In your service:
```csharp
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
// Optional: Lock the transportation row to prevent race
await _dbContext.Database.ExecuteSqlRawAsync(
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', @LockMode = 'Exclusive', @LockTimeout = 10000;");
// Re-fetch transportation + tickets
var transportation = await _transportationRepository.GetByIdWithTickets(dto.TransportationId);
// Validate seats
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
if (!string.IsNullOrEmpty(seatCheck)) return Result<long>.Error(0, seatCheck);
// Proceed with reservation
...
await _unitOfWork.SaveChangesAsync();
await transaction.CommitAsync();
```
---
## 🧠 Summary Table
|Strategy|Prevents Race?|Web-farm Safe|Recommended For|
|---|---|---|---|
|`sp_getapplock`|✅ Yes|✅ Yes|SQL Server web apps|
|EF raw SQL `WITH (UPDLOCK)`|✅ Yes|✅ Yes|SQL Server monoliths|
|`SemaphoreSlim`|✅ Yes|❌ No|Local-only apps|
|`FOR UPDATE`|✅ Yes|✅ Yes|PostgreSQL, MySQL|
---
Let me know your DB provider and environment (single-instance or multi-instance), and I can generate exact code tailored for that.
Perfect — you're absolutely right to want **the whole process to be atomic**:
**Seat allocation**,
**Ticket creation**,
**Balance checking & payment**,
**Transaction creation**,
**Traveler creation/upsert**, etc.
This needs more than just _database-level locks_. You want to **serialize execution** for the same transportation to avoid concurrency conflicts — **across the entire flow**, not just the DB writes.
---
## ✅ Your Goal
> **For a given `TransportationId`, only one call to `CreateTicketOrderAsync` should execute at a time**, regardless of app instance or user.
This includes logic **outside EF** (like payment, seat assignment, person creation, etc.).
---
## 🛠️ The Right Solution: **Distributed Application Locking**
### Use **SQL Servers `sp_getapplock`** to lock **by `TransportationId`**
Why?
- It locks **application-level logic**, not just SQL rows
- It works across threads, processes, and even app servers
- It ensures only **one process** can work on that transportation at a time
---
## ✅ How to Implement in Your Method
### 🔒 Step 1: Lock by Transportation ID
Add this at the **start** of your method:
```csharp
await _dbContext.Database.ExecuteSqlRawAsync(
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', " +
"@LockMode = 'Exclusive', @LockTimeout = 10000;");
```
This ensures that **only one call per transportation** runs at a time.
If a second request tries to lock the same ID, it waits up to 10 seconds.
---
### 🔁 Step 2: Begin EF Transaction
This will include all EF operations in a single atomic unit:
```csharp
using var transaction = await _dbContext.Database.BeginTransactionAsync();
```
---
### 🧠 Step 3: Keep the Full Logic As Is, But Within Transaction
Like this:
```csharp
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
{
// 🔒 Lock to prevent concurrency per transportation
await _dbContext.Database.ExecuteSqlRawAsync(
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', " +
"@LockMode = 'Exclusive', @LockTimeout = 10000;");
// 🧾 Begin transaction
using var transaction = await _dbContext.Database.BeginTransactionAsync();
// 💰 Load account and transportation
var account = await _accountRepository.GetByIdAsync(accountId);
if (account == null) return Result<long>.Error(0, "Account not found");
var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
if (transportation == null) return Result<long>.Error(0, "Transportation not found");
// 💵 Check balance
var price = transportation.BasePrice * dto.Travelers.Count;
if (account.CurrentBalance < price)
return Result<long>.Error(0, "Not enough money");
// ✅ Validate seat availability
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
if (!string.IsNullOrEmpty(seatCheck))
return Result<long>.Error(0, seatCheck);
// 🪑 Assign seats (if dynamic logic)
await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travelers);
// 👤 Upsert travelers (can also use same transaction)
await UpsertTravelers(account.Id, dto.Travelers);
// 🎟️ Create ticket order
var ticketOrder = new TicketOrder
{
BuyerId = account.Id,
CreatedAt = DateTime.UtcNow,
Description = "",
SerialNumber = Guid.NewGuid().ToString("N"),
TransportationId = dto.TransportationId,
};
await _ticketOrderRepository.AddAsync(ticketOrder);
// 🎫 Add tickets
foreach (var traveler in dto.Travelers)
{
var ticket = new Ticket
{
CreatedAt = DateTime.UtcNow,
Description = traveler.Description,
SeatId = traveler.SeatId!.Value,
SerialNumber = Guid.NewGuid().ToString("N"),
TicketOrder = ticketOrder,
TicketStatusId = 1,
TravelerId = traveler.Id,
};
await _ticketRepository.AddAsync(ticket);
}
// 💳 Commit everything
await _unitOfWork.SaveChangesAsync();
// 💸 Withdraw money and create transaction
await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id, price);
// ✅ Commit transaction
await transaction.CommitAsync();
return Result<long>.Success(ticketOrder.Id);
}
```
---
## 🧱 Summary of Protection
|Step|Protected?|How|
|---|---|---|
|Prevent concurrent seat assignment|✅|`sp_getapplock` per `TransportationId`|
|Prevent concurrent DB changes|✅|EF `DbTransaction`|
|Prevent double-spend|✅|Balance check inside the same lock + transaction|
|Ensure traveler upserts/tickets are safe|✅|Wrapped in same transaction|
---
## ⚠️ Important Note
Make sure all the operations (`AssignSeatsIfDynamic`, `UpsertTravelers`, etc.):
- Use the **same `DbContext`/UnitOfWork**
- Dont commit changes prematurely (no `SaveChangesAsync()` before you're ready to commit the whole transaction)
---
## ✅ Optional (Safety): Add retry logic
In case of a **deadlock or lock timeout**, you might want to retry the whole operation once.
---
Let me know if you want me to extract this into a reusable helper like:
```csharp
await _appLockService.ExecuteWithLockAsync($"transportation_lock_{dto.TransportationId}", async () => {
// your reservation logic
});
```
That would clean things up nicely.
Sure. Lets break down everything you need to know about **transactions** in Entity Framework Core (EF Core) — especially for your case where you're doing **complex operations that must all succeed or fail together**.
---
## 🧾 What is a Transaction?
A **transaction** is a unit of work that is treated as a **single, indivisible operation**. Either **all of it succeeds** (committed), or **none of it does** (rolled back).
### 🧠 Properties of a transaction (ACID):
|Property|Meaning|
|---|---|
|**Atomicity**|All steps succeed or none do|
|**Consistency**|DB remains valid before and after|
|**Isolation**|Transactions dont interfere with each other|
|**Durability**|Once committed, data is persisted permanently|
---
## ✅ Why You Need Transactions
In your case, youre:
- Assigning seats
- Creating or updating people
- Creating a `TicketOrder`
- Creating multiple `Tickets`
- Updating account balance & transaction history
All of these must either:
- Complete **fully and safely**, or
- **Fail entirely**, with no partial data left behind.
---
## 🧰 Transaction in EF Core: Full Example
### 🔧 Step-by-step Example:
```csharp
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
{
// Begin EF Core transaction
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// Load required entities
var account = await _accountRepository.GetByIdAsync(accountId);
if (account == null)
return Result<long>.Error(0, "Account not found");
var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
if (transportation == null)
return Result<long>.Error(0, "Transportation not found");
var price = transportation.BasePrice * dto.Travelers.Count;
if (account.CurrentBalance < price)
return Result<long>.Error(0, "Not enough money");
// Validate seat assignment
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
if (!string.IsNullOrEmpty(seatCheck))
return Result<long>.Error(0, seatCheck);
await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travelers);
await UpsertTravelers(account.Id, dto.Travelers);
// Create ticket order
var ticketOrder = new TicketOrder
{
BuyerId = account.Id,
CreatedAt = DateTime.UtcNow,
SerialNumber = Guid.NewGuid().ToString("N"),
TransportationId = dto.TransportationId,
};
await _ticketOrderRepository.AddAsync(ticketOrder);
foreach (var traveler in dto.Travelers)
{
await _ticketRepository.AddAsync(new Ticket
{
CreatedAt = DateTime.UtcNow,
Description = traveler.Description,
SeatId = traveler.SeatId.Value,
SerialNumber = Guid.NewGuid().ToString("N"),
TicketOrder = ticketOrder,
TicketStatusId = 1,
TravelerId = traveler.Id,
});
}
// Save changes
await _unitOfWork.SaveChangesAsync();
// Withdraw money from account
await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id, price);
// Commit transaction
await transaction.CommitAsync();
return Result<long>.Success(ticketOrder.Id);
}
catch (Exception ex)
{
// Rollback if anything failed
await transaction.RollbackAsync();
return Result<long>.Error(0, "Reservation failed: " + ex.Message);
}
}
```
---
## 🔄 Isolation Levels (Advanced)
### You can specify isolation level when beginning a transaction:
```csharp
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
```
### Common levels:
|Isolation Level|Description|Use Case|
|---|---|---|
|`ReadCommitted` (default)|No dirty reads|Usually enough|
|`RepeatableRead`|Prevents non-repeatable reads|Seat assignment logic|
|`Serializable`|Full isolation (slower, safer)|Financial or reservation systems|
---
## 🧩 When `SaveChangesAsync()` Commits vs. Not
Calling `SaveChangesAsync()` **does not commit** the outer transaction — it just writes pending changes to the database.
The real commit happens only when you call:
```csharp
await transaction.CommitAsync();
```
If anything fails before that, the changes are **not persisted**.
---
## ✅ Best Practices for Transactions in EF Core
|Best Practice|Why|
|---|---|
|Keep transactions **short**|Locks and contention increase with time|
|Catch exceptions and **rollback**|Prevent dirty/incomplete data|
|Use **only one DbContext** inside the transaction|EF tracks entities via one unit of work|
|Dont call SaveChanges too early|May break the atomicity|
|Use **appropriate isolation level**|Based on your concurrency needs|
|Consider retry policy for transient errors|Especially in distributed systems|
---
## ❗ Common Pitfalls
|Mistake|Problem|
|---|---|
|Mixing DbContext instances inside one transaction|Breaks consistency|
|Committing too early|Later failure still leaves partial data|
|Forgetting rollback in catch|Leaves transaction open or inconsistent|
|Async code outside transaction scope|Could break atomicity or introduce race|
---
## 🧠 Summary: Transaction Essentials
|Concept|Description|
|---|---|
|`BeginTransactionAsync()`|Starts a manual transaction|
|`SaveChangesAsync()`|Writes changes but doesnt commit outer transaction|
|`CommitAsync()`|Finalizes all DB changes|
|`RollbackAsync()`|Reverts all changes since the transaction began|
|Scope|All EF/DB actions done within the transaction block|
---
If you'd like, I can create a **generic transaction helper**, or even make this a **middleware** or **interceptor**, depending on how reusable you want this to be.
Let me know!
Great! These questions show deep thinking about **how transactions and locks actually work** in EF Core and layered architecture. Lets answer them one by one with real clarity and direction.
---
## ❓ 1. _"I have this method in my `TicketOrderService`, and I don't have access to `_dbContext`."_
### ✅ Solution:
If you're using **Repository + UnitOfWork pattern**, your `UnitOfWork` should expose a method to access the underlying **DbContext** or allow you to start a transaction.
**👉 Extend your `IUnitOfWork` interface like this:**
```csharp
public interface IUnitOfWork
{
Task<int> SaveChangesAsync();
Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = IsolationLevel.ReadCommitted);
DbContext Context { get; } // optional
}
```
And in your implementation:
```csharp
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context) => _context = context;
public Task<int> SaveChangesAsync() => _context.SaveChangesAsync();
public Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = IsolationLevel.ReadCommitted)
=> _context.Database.BeginTransactionAsync(level);
public DbContext Context => _context;
}
```
### ✅ Then in your `TicketOrderService`:
```csharp
using var transaction = await _unitOfWork.BeginTransactionAsync();
// your operations
await _unitOfWork.SaveChangesAsync();
await transaction.CommitAsync();
```
This keeps your `Service` clean and architecture-respecting.
---
## ❓ 2. _"How does the transaction hold stuff? Wouldnt `unitOfWork.SaveChangesAsync()` just save everything?"_
### 🔍 Clarification:
- `SaveChangesAsync()` **writes the changes to the database**, but…
- **If you're in a transaction**, the database holds those changes in a **temporary, invisible state** until you explicitly call:
```csharp
await transaction.CommitAsync();
```
💡 If you **dont commit**, or you **rollback**, then **those changes are discarded**, even if you called `SaveChangesAsync()`.
### 👇 Timeline of how EF works in a transaction:
1. EF tracks entities in memory.
2. You call `SaveChangesAsync()`.
3. Changes are written to the DB — but:
- If inside a transaction, they are **not visible to others yet**.
- They are held until `CommitAsync()`.
4. If you call `RollbackAsync()`, all writes are undone.
---
## ❓ 3. _"How would the lock know that the work with that transportation is finished?"_
### 🔒 About `sp_getapplock`:
This SQL Server feature locks a **named resource** (like `transportation_lock_123`) for **the duration of the current SQL connection and transaction**.
Heres how it works:
|Action|Result|
|---|---|
|You call `sp_getapplock`|SQL Server grants an exclusive lock for the resource|
|You complete the transaction (commit or rollback)|Lock is **automatically released**|
|Your connection is closed or disposed|Lock is released|
### 🔍 So you dont need to manually release the lock.
Just make sure:
- The **lock and transaction use the same connection**
- You commit or rollback after the work is done
✅ EF Core handles all that automatically when using `Database.BeginTransactionAsync()`.
---
## ✅ Final Advice Recap
|Question|Answer|
|---|---|
|I dont have access to `_dbContext`|Expose `BeginTransactionAsync` on your `IUnitOfWork`|
|How does transaction matter if I save mid-way?|It isolates changes — no one sees them until committed|
|How does lock get released?|Automatically when transaction completes or connection closes|
---
Let me know if you want:
- Sample `UnitOfWork` and `Repository` setup for full support
- A lock + transaction helper method
- Unit tests to simulate concurrent reservations
You're doing great — this is how robust, real-world software gets built.
You've unpacked crucial concerns around **transactions**, **unit-of-work (UoW)**, and **application-wide consistency**. Here's a polished breakdown with clear answers and guidance:
---
### ✅ 1. Is it okay to use both Transactions **and** Unit of Work?
- **Yes, absolutely.** UoW is a design pattern that groups multiple repository operations into one logical unit, while a _transaction_ ensures these operations either all succeed or all fail atomically ([antondevtips.com](https://antondevtips.com/blog/implementing-unit-of-work-pattern-in-ef-core?utm_source=chatgpt.com "Implementing Unit of Work Pattern in EF Core - Anton DevTips")).
- In EF Core, the **DbContext itself implements UoW**, collecting changes in memory until `SaveChangesAsync()` is called ([reddit.com](https://www.reddit.com/r/dotnet/comments/17npjtx/unit_of_work_pattern_still_necessary_in_net_8/?utm_source=chatgpt.com "Unit of Work Pattern still necessary in .NET 8? : r/dotnet - Reddit")).
- When you execute **multiple SaveChanges**, or include non-repository logic (e.g., balance updates, payment creation, seat locking), **explicit transactions** ensure the entire flow remains atomic .
---
### ✅ 2. How exactly to open/close transactions? Where to be careful?
#### ✨ Use EF to manage transactions in your UoW:
1. **Extend your `IUnitOfWork`** to include transaction methods:
```csharp
Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = ReadCommitted);
Task<int> SaveChangesAsync();
```
2. **In your service**, do:
```csharp
using var tx = await _unitOfWork.BeginTransactionAsync();
try {
// perform all operations (seat locks, entity changes, balance, tickets)
await _unitOfWork.SaveChangesAsync();
await tx.CommitAsync();
} catch {
await tx.RollbackAsync();
throw;
}
```
3. **Know the boundaries**: Keep the transaction as short as possible. Open it before critical operations and close it after the final `CommitAsync()`.
> 💡 Use `(IsolationLevel.Serializable)` for scenarios like seat reservation where concurrent writes must be prevented ([milanjovanovic.tech](https://www.milanjovanovic.tech/blog/working-with-transactions-in-ef-core?utm_source=chatgpt.com "Working With Transactions In EF Core"), [learn.microsoft.com](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application?utm_source=chatgpt.com "Implementing the Repository and Unit of Work Patterns in an ASP ...")).
---
### ✅ 3. Why did things “work” without explicit transactions?
- **Simple SaveChanges** calls act like atomic units—if they fail, nothing is saved. With just one SaveChanges call, EF wraps it in a default transaction ([stackoverflow.com](https://stackoverflow.com/questions/24636352/unit-of-work-i-dont-need-to-use-transactions?utm_source=chatgpt.com "c# - unit of work - I don't need to use transactions? - Stack Overflow")).
- However, as soon as your workflow involves **multiple SaveChanges**, **external services**, or **cross-repository updates**, the default behavior is no longer sufficient. A failure midway could leave your data in a partial state.
- That's where **explicit transaction scopes** come in—to guarantee everything is batched and rolled back if any part fails ([antondevtips.com](https://antondevtips.com/blog/implementing-unit-of-work-pattern-in-ef-core?utm_source=chatgpt.com "Implementing Unit of Work Pattern in EF Core - Anton DevTips")).
---
### 🧠 TL;DR Summary
|Concept|Purpose|When Needed|
|---|---|---|
|**Unit of Work**|Organizes multiple EF operations into one commit|Always used with EF DbContext|
|**Implicit transaction**|EF wraps each SaveChanges automatically|Ok for single SaveChanges calls|
|**Explicit transaction**|Manually control commit/rollback across many operations|Needed when multiple SaveChanges, external services, concurrency, etc.|
|**IsolationLevel.Serializable**|Highest locking level to prevent concurrent conflicts|Use for critical resource locks (e.g., seat booking)|
|**Be careful**|Keep transactions short to avoid locks and deadlocks|Avoid long-running operations inside the transaction|
---
If you want, I can provide:
- **Code templates** for UoW with transaction management
- **Interceptor or decorator** to automate transaction handling in your services
- **Concurrency demos** to test seat reservation under load
Just say the word!
Absolutely! Lets clarify both of your statements with authoritative sources:
---
## 1. If I haven't called `BeginTransaction` and call `SaveChanges`, will changes be saved?
✅ **Yes.** EF Core automatically wraps **every** `SaveChanges()` call in its **own transaction**. This means:
- If **all operations succeed**, the data is saved.
- If **anything fails**, the entire set is **rolled back**, leaving the DB unchanged. citeturn0search0turn0search9
🔁 So even without explicitly starting a transaction, `SaveChanges()` ensures atomicity for that batch of changes.
---
## 2. If I have called `BeginTransaction`, and then call `SaveChanges`, do changes go to the DB immediately?
✅ **Yes, but context matters.** When you wrap operations in an explicit transaction:
- Each `SaveChanges()` writes within that transaction.
- However, **none** of these writes become visible to other connections until you call `Commit()`.
EF also uses **savepoints** internally (for multiple `SaveChanges()` calls) to enable partial rollbacks without rolling back the entire transaction. citeturn0search0turn0search2turn0search3
---
### 🧩 In Plain English
| Scenario | SaveChanges Only | BeginTransaction + SaveChanges |
|------------------|--------------------------------------------------|----------------------------------------------------|
| No transaction | Auto-wrapped in its own transactionimmediate. | — |
| With explicit transaction | Writes are within the transaction, not yet committed. | ✔ |
| Rollback before commit | Not possible. SaveChanges is final. | ✔ Rolls everything back. |
---
### ✅ TL;DR
- ✅ **Without `BeginTransaction`**: `SaveChanges()` is atomic per call—commits or rolls back automatically.
- ✅ **With `BeginTransaction`**: `SaveChanges()` writes into the transaction, but the changes are **held until** you call `Commit()`; `Rollback()` discards them all.
---
Want some **ready-to-use code** to introduce `BeginTransaction()` into your UnitOfWork and services? Just let me know and Ill supply a clean implementation!