π Multi-DataSource UnitOfWork
Coordinate operations across multiple data sources with relationship management and transaction coordination
DataManagementEngineStandard/Editor/MultiDataSourceUnitOfWork.cs
π Table of Contents
Overview
The MultiDataSourceUnitOfWork class provides a unified interface for managing entities across multiple data sources
while maintaining relationships and ensuring data consistency. It orchestrates multiple UnitOfWork instances and
handles cross-datasource transactions, enabling you to work with complex data architectures seamlessly.
- Unified Interface: Work with multiple data sources through a single API
- Automatic Relationship Management: Define relationships once, navigate automatically
- Transaction Coordination: Atomic commits across all data sources
- Parent-Child Navigation: Automatic loading of related entities
- Cross-Source Relationships: Maintain referential integrity across different databases
- Validation & Rollback: Comprehensive error handling and rollback capabilities
π Cross-Platform Support
Connect SQL Server, PostgreSQL, MongoDB, SQLite, and more in a single transaction
β‘ Automatic Loading
Related entities are loaded automatically when navigating parent records
π‘οΈ Data Integrity
Enforce referential integrity and business rules across multiple systems
π― Event-Driven
Responsive UI updates through comprehensive event notifications
Signature-Verified Snapshot
public partial class MultiDataSourceUnitOfWork : IDisposable
{
public event EventHandler<ChildDataChangedEventArgs> ChildDataChanged;
public dynamic CurrentParentEntity => _currentParentEntity;
public Task AddUnitOfWorkAsync<T>(string dataSourceName, string entityName, string primaryKey)
where T : Entity, new();
public void AddRelationship(string parentEntityName, string childEntityName, string parentKeyField,
string childForeignKeyField, RelationshipBehavior deleteBehavior = RelationshipBehavior.CascadeDelete,
bool isRequired = false);
public ObservableBindingList<T> GetEntities<T>(string entityName) where T : Entity, new();
public Task<Dictionary<string, ObservableBindingList<TChild>>> GetAllRelatedChildrenAsync<TChild, TParent>(string parentEntityName, TParent parentEntity)
where TChild : Entity, new()
where TParent : Entity, new();
public Task<IErrorsInfo> InsertAsync<T>(string entityName, T entity) where T : Entity, new();
public Task<IErrorsInfo> UpdateAsync<T>(string entityName, T entity) where T : Entity, new();
public Task<IErrorsInfo> DeleteAsync<T>(string entityName, T entity) where T : Entity, new();
public Task<IErrorsInfo> CommitAsync();
}
π§ Constructor
Basic Initialization
public MultiDataSourceUnitOfWork(IDMEEditor dmeEditor)
Initializes a new instance of the MultiDataSourceUnitOfWork class.
| Parameter | Type | Description |
|---|---|---|
| dmeEditor | IDMEEditor | The data management editor instance providing access to data sources and configuration |
Basic Usage
// Initialize the multi-datasource UnitOfWork
using var multiUoW = new MultiDataSourceUnitOfWork(dmeEditor);
// Now ready to add entities and relationships
Console.WriteLine("Multi-DataSource UnitOfWork initialized successfully");
π§ Setting Up Entities & Relationships
Adding Entity Types
public async Task AddUnitOfWorkAsync<T>(string dataSourceName, string entityName, string primaryKey)
where T : Entity, new()
Adds a new UnitOfWork for a specific entity type and data source.
| Parameter | Type | Description |
|---|---|---|
| dataSourceName | string | Name of the data source (must exist in DME configuration) |
| entityName | string | Name of the entity/table |
| primaryKey | string | Name of the primary key field |
Adding Multiple Entity Types
// Set up entities from different data sources
await multiUoW.AddUnitOfWorkAsync<Customer>("SqlServer_CRM", "Customers", "CustomerID");
await multiUoW.AddUnitOfWorkAsync<Order>("MongoDB_Orders", "Orders", "OrderID");
await multiUoW.AddUnitOfWorkAsync<OrderDetail>("MongoDB_Orders", "OrderDetails", "DetailID");
await multiUoW.AddUnitOfWorkAsync<Product>("PostgreSQL_Inventory", "Products", "ProductID");
await multiUoW.AddUnitOfWorkAsync<Address>("SqlServer_CRM", "Addresses", "AddressID");
Console.WriteLine("All entity types configured across multiple data sources");
Defining Relationships
public void AddRelationship(
string parentEntityName,
string childEntityName,
string parentKeyField,
string childForeignKeyField,
RelationshipBehavior deleteBehavior = RelationshipBehavior.CascadeDelete,
bool isRequired = false)
| Parameter | Type | Description |
|---|---|---|
| parentEntityName | string | Name of the parent entity |
| childEntityName | string | Name of the child entity |
| parentKeyField | string | Primary key field in parent entity |
| childForeignKeyField | string | Foreign key field in child entity |
| deleteBehavior | RelationshipBehavior | How to handle deletes (CascadeDelete, SetNull, Restrict) |
| isRequired | bool | Whether the relationship is required for validation |
Comprehensive Relationship Setup
// Define Customer -> Orders relationship (across SQL Server and MongoDB)
multiUoW.AddRelationship(
"Customer", "Order", // Parent -> Child
"CustomerID", "CustomerID", // Key mapping
RelationshipBehavior.CascadeDelete, // Delete orders when customer is deleted
isRequired: false // Customers can exist without orders
);
// Define Customer -> Addresses relationship (within SQL Server)
multiUoW.AddRelationship(
"Customer", "Address",
"CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete,
isRequired: false
);
// Define Order -> OrderDetails relationship (within MongoDB)
multiUoW.AddRelationship(
"Order", "OrderDetail",
"OrderID", "OrderID",
RelationshipBehavior.CascadeDelete,
isRequired: true // Orders must have at least one detail
);
// Define OrderDetail -> Product relationship (across MongoDB and PostgreSQL)
multiUoW.AddRelationship(
"Product", "OrderDetail", // Product is parent of OrderDetail
"ProductID", "ProductID",
RelationshipBehavior.Restrict, // Can't delete products that are referenced
isRequired: true // OrderDetails must reference a valid product
);
Console.WriteLine("Cross-datasource relationships configured successfully");
π Data Access Methods
Getting Entity Collections
public ObservableBindingList<T> GetEntities<T>(string entityName) where T : Entity, new()
Gets the observable collection of entities for a specific entity type.
Getting Related Children
public async Task<Dictionary<string, ObservableBindingList<TChild>>> GetAllRelatedChildrenAsync<TChild, TParent>(
string parentEntityName,
TParent parentEntity)
Retrieves all related child entities for a given parent entity across all defined relationships.
Data Access Examples
// Get entity collections
var customers = multiUoW.GetEntities<Customer>("Customer");
var orders = multiUoW.GetEntities<Order>("Order");
var products = multiUoW.GetEntities<Product>("Product");
Console.WriteLine($"Loaded {customers.Count} customers, {orders.Count} orders, {products.Count} products");
// Get all related children for a specific customer
var selectedCustomer = customers.FirstOrDefault(c => c.CustomerID == "CUST001");
if (selectedCustomer != null)
{
// This will return a dictionary with all related child collections
var relatedChildren = await multiUoW.GetAllRelatedChildrenAsync<object, Customer>(
"Customer", selectedCustomer);
// Access specific child collections
if (relatedChildren.ContainsKey("Order"))
{
var customerOrders = relatedChildren["Order"];
Console.WriteLine($"Customer {selectedCustomer.Name} has {customerOrders.Count} orders");
}
if (relatedChildren.ContainsKey("Address"))
{
var customerAddresses = relatedChildren["Address"];
Console.WriteLine($"Customer {selectedCustomer.Name} has {customerAddresses.Count} addresses");
}
}
// Access current parent entity (for navigation scenarios)
var currentParent = multiUoW.CurrentParentEntity;
if (currentParent != null)
{
Console.WriteLine($"Current parent: {currentParent}");
}
- Entity collections are cached and reused across calls
- Related children are loaded on-demand when navigating
- Use specific generic types when possible for better performance
- Consider using filters at the data source level for large datasets
π CRUD Operations
Insert Operations
public async Task<IErrorsInfo> InsertAsync<T>(string entityName, T entity) where T : Entity, new()
Inserts a new entity and handles any related entities according to defined relationships.
Update Operations
public async Task<IErrorsInfo> UpdateAsync<T>(string entityName, T entity) where T : Entity, new()
Updates an existing entity and triggers relationship-based updates if needed.
Delete Operations
public async Task<IErrorsInfo> DeleteAsync<T>(string entityName, T entity) where T : Entity, new()
Deletes an entity and handles related entities based on the defined relationship behavior.
Complete CRUD Example
// INSERT: Create a new customer with address
var newCustomer = new Customer
{
CustomerID = Guid.NewGuid().ToString(),
Name = "John Doe",
Email = "john@example.com",
Status = "Active"
};
var insertResult = await multiUoW.InsertAsync("Customer", newCustomer);
if (insertResult.Flag == Errors.Ok)
{
Console.WriteLine($"Customer {newCustomer.Name} inserted successfully");
// Add an address for the customer
var address = new Address
{
AddressID = Guid.NewGuid().ToString(),
CustomerID = newCustomer.CustomerID,
Street = "123 Main St",
City = "Anytown",
State = "CA",
ZipCode = "12345"
};
await multiUoW.InsertAsync("Address", address);
Console.WriteLine("Customer address added");
}
// UPDATE: Modify customer information
newCustomer.Email = "john.doe@example.com";
newCustomer.Status = "Premium";
var updateResult = await multiUoW.UpdateAsync("Customer", newCustomer);
if (updateResult.Flag == Errors.Ok)
{
Console.WriteLine("Customer updated successfully");
}
// INSERT: Create an order for the customer
var newOrder = new Order
{
OrderID = Guid.NewGuid().ToString(),
CustomerID = newCustomer.CustomerID,
OrderDate = DateTime.Now,
Status = "Pending",
Total = 299.99m
};
await multiUoW.InsertAsync("Order", newOrder);
// INSERT: Add order details
var orderDetail = new OrderDetail
{
DetailID = Guid.NewGuid().ToString(),
OrderID = newOrder.OrderID,
ProductID = "PROD001", // Assuming this product exists
Quantity = 2,
UnitPrice = 149.99m
};
await multiUoW.InsertAsync("OrderDetail", orderDetail);
Console.WriteLine("Order with details created");
// DELETE: Remove an order (will cascade to details if configured)
var orderToDelete = orders.FirstOrDefault(o => o.Status == "Cancelled");
if (orderToDelete != null)
{
var deleteResult = await multiUoW.DeleteAsync("Order", orderToDelete);
if (deleteResult.Flag == Errors.Ok)
{
Console.WriteLine("Cancelled order deleted (with cascade to details)");
}
}
- All operations respect the defined relationships and behaviors
- Changes are tracked but not persisted until
CommitAsync()is called - Failed operations will provide detailed error information
- Use the entity name exactly as defined when adding the UnitOfWork
Γ°ΕΈβΒΎ Transaction Management
Atomic Commits
public async Task<IErrorsInfo> CommitAsync()
Commits all changes across all managed UnitOfWork instances. If any commit fails, all previously committed changes are rolled back.
Transaction Safety Example
try
{
// Make multiple changes across different data sources
var customer = new Customer
{
CustomerID = "CUST123",
Name = "Jane Smith",
Email = "jane@example.com"
};
await multiUoW.InsertAsync("Customer", customer);
var order = new Order
{
OrderID = "ORD456",
CustomerID = customer.CustomerID,
OrderDate = DateTime.Now,
Total = 150.00m
};
await multiUoW.InsertAsync("Order", order);
var address = new Address
{
AddressID = "ADDR789",
CustomerID = customer.CustomerID,
Street = "456 Oak Ave",
City = "Somewhere"
};
await multiUoW.InsertAsync("Address", address);
// Commit all changes atomically across all data sources
var result = await multiUoW.CommitAsync();
if (result.Flag == Errors.Ok)
{
Console.WriteLine("β
All changes committed successfully across:");
Console.WriteLine(" - SQL Server (Customer, Address)");
Console.WriteLine(" - MongoDB (Order)");
Console.WriteLine(" - Transaction completed atomically");
}
else
{
Console.WriteLine($"β Commit failed: {result.Message}");
Console.WriteLine("All changes have been rolled back automatically");
// Detailed error information
if (result.Ex != null)
{
Console.WriteLine($"Exception details: {result.Ex.Message}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"β Transaction error: {ex.Message}");
Console.WriteLine("All changes have been rolled back automatically");
}
- Atomicity: All changes succeed or all fail
- Consistency: Relationships are validated before commit
- Isolation: Changes are isolated until commit
- Durability: Committed changes are persisted across all data sources
If any individual UnitOfWork fails to commit, the system automatically rolls back all previously committed changes to maintain consistency across all data sources. This ensures your data remains in a valid state even when working with multiple databases.
π Relationship Behaviors
The MultiDataSourceUnitOfWork supports three relationship behaviors that determine how related entities are handled during delete operations:
ποΈ CascadeDelete
When a parent entity is deleted, all related child entities are automatically deleted
// Customer -> Orders with cascade delete
multiUoW.AddRelationship(
"Customer", "Order",
"CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete
);
// Deleting customer will delete all their orders
Γ°ΕΈΕ‘Β« Restrict
Prevents deletion of a parent entity if related child entities exist
// Product -> OrderDetails with restrict
multiUoW.AddRelationship(
"Product", "OrderDetail",
"ProductID", "ProductID",
RelationshipBehavior.Restrict
);
// Cannot delete products that are referenced in orders
π SetNull
When a parent entity is deleted, the foreign key fields in related child entities are set to null
// Manager -> Employees with set null
multiUoW.AddRelationship(
"Manager", "Employee",
"ManagerID", "ManagerID",
RelationshipBehavior.SetNull
);
// Deleting manager sets ManagerID to null in employees
Relationship Behavior in Action
// Set up different relationship behaviors
multiUoW.AddRelationship("Customer", "Order", "CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete);
multiUoW.AddRelationship("Order", "OrderDetail", "OrderID", "OrderID",
RelationshipBehavior.CascadeDelete);
multiUoW.AddRelationship("Product", "OrderDetail", "ProductID", "ProductID",
RelationshipBehavior.Restrict);
// Attempt to delete a customer
var customerToDelete = customers.FirstOrDefault(c => c.Status == "Inactive");
if (customerToDelete != null)
{
try
{
var result = await multiUoW.DeleteAsync("Customer", customerToDelete);
if (result.Flag == Errors.Ok)
{
Console.WriteLine("β
Customer deleted successfully");
Console.WriteLine(" Γ’β β All customer orders were cascade deleted");
Console.WriteLine(" Γ’β β All order details were cascade deleted");
}
else
{
Console.WriteLine($"β Delete failed: {result.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"β Delete prevented by relationship constraint: {ex.Message}");
}
}
// Attempt to delete a product that's referenced in orders
var productToDelete = products.FirstOrDefault();
if (productToDelete != null)
{
try
{
var result = await multiUoW.DeleteAsync("Product", productToDelete);
if (result.Flag != Errors.Ok)
{
Console.WriteLine($"β Product deletion restricted: {result.Message}");
Console.WriteLine(" Γ’β β Product is referenced in existing order details");
}
}
catch (Exception ex)
{
Console.WriteLine($"β Delete prevented: {ex.Message}");
}
}
- SetNull only works with nullable foreign key fields
- For non-nullable fields, the behavior will fall back to Restrict
- Always consider the business implications of orphaned records
- Test relationship behaviors thoroughly in development
Γ°ΕΈΕΕΈ Real-World Example: E-Commerce System
Complete E-Commerce Implementation
public class ECommerceService : IDisposable
{
private readonly MultiDataSourceUnitOfWork _multiUoW;
public ECommerceService(IDMEEditor dmeEditor)
{
_multiUoW = new MultiDataSourceUnitOfWork(dmeEditor);
SetupDataStructure().Wait();
}
private async Task SetupDataStructure()
{
// Setup entities across multiple data sources
await _multiUoW.AddUnitOfWorkAsync<Customer>("CRM_SqlServer", "Customers", "CustomerID");
await _multiUoW.AddUnitOfWorkAsync<Order>("Orders_MongoDB", "Orders", "OrderID");
await _multiUoW.AddUnitOfWorkAsync<OrderDetail>("Orders_MongoDB", "OrderDetails", "DetailID");
await _multiUoW.AddUnitOfWorkAsync<Product>("Inventory_PostgreSQL", "Products", "ProductID");
await _multiUoW.AddUnitOfWorkAsync<Address>("CRM_SqlServer", "Addresses", "AddressID");
await _multiUoW.AddUnitOfWorkAsync<Payment>("Payments_Firebird", "Payments", "PaymentID");
// Define business relationships
_multiUoW.AddRelationship("Customer", "Order", "CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete, false);
_multiUoW.AddRelationship("Customer", "Address", "CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete, false);
_multiUoW.AddRelationship("Order", "OrderDetail", "OrderID", "OrderID",
RelationshipBehavior.CascadeDelete, true);
_multiUoW.AddRelationship("Order", "Payment", "OrderID", "OrderID",
RelationshipBehavior.Restrict, false);
_multiUoW.AddRelationship("Product", "OrderDetail", "ProductID", "ProductID",
RelationshipBehavior.Restrict, true);
// Setup event handling
_multiUoW.ChildDataChanged += OnChildDataChanged;
Console.WriteLine("π― E-Commerce data structure configured across:");
Console.WriteLine(" π SQL Server: Customers, Addresses");
Console.WriteLine(" π MongoDB: Orders, OrderDetails");
Console.WriteLine(" π PostgreSQL: Products");
Console.WriteLine(" π Firebird: Payments");
}
private async void OnChildDataChanged(object sender, ChildDataChangedEventArgs e)
{
Console.WriteLine($"π Data changed: {e.ChildEntityName} for {e.ParentEntityName}");
// Update UI or trigger business logic
switch (e.ChildEntityName)
{
case "Order":
await UpdateCustomerOrderSummary();
break;
case "Address":
await ValidateShippingOptions();
break;
case "Payment":
await UpdateOrderStatus();
break;
}
}
public async Task<string> ProcessOrderAsync(OrderRequest request)
{
try
{
// 1. Validate customer
var customers = _multiUoW.GetEntities<Customer>("Customer");
var customer = customers.FirstOrDefault(c => c.CustomerID == request.CustomerID);
if (customer == null)
{
throw new BusinessException("Customer not found");
}
// 2. Create order
var order = new Order
{
OrderID = Guid.NewGuid().ToString(),
CustomerID = request.CustomerID,
OrderDate = DateTime.Now,
Status = "Pending",
ShippingAddress = request.ShippingAddress,
Total = 0
};
await _multiUoW.InsertAsync("Order", order);
// 3. Add order details and calculate total
decimal orderTotal = 0;
var products = _multiUoW.GetEntities<Product>("Product");
foreach (var item in request.Items)
{
var product = products.FirstOrDefault(p => p.ProductID == item.ProductID);
if (product == null)
{
throw new BusinessException($"Product {item.ProductID} not found");
}
if (product.StockQuantity < item.Quantity)
{
throw new BusinessException($"Insufficient stock for {product.Name}");
}
var detail = new OrderDetail
{
DetailID = Guid.NewGuid().ToString(),
OrderID = order.OrderID,
ProductID = item.ProductID,
Quantity = item.Quantity,
UnitPrice = product.Price,
LineTotal = item.Quantity * product.Price
};
await _multiUoW.InsertAsync("OrderDetail", detail);
orderTotal += detail.LineTotal;
// Update product stock
product.StockQuantity -= item.Quantity;
await _multiUoW.UpdateAsync("Product", product);
}
// 4. Update order total
order.Total = orderTotal;
await _multiUoW.UpdateAsync("Order", order);
// 5. Process payment
var payment = new Payment
{
PaymentID = Guid.NewGuid().ToString(),
OrderID = order.OrderID,
Amount = orderTotal,
PaymentMethod = request.PaymentMethod,
PaymentDate = DateTime.Now,
Status = "Processed"
};
await _multiUoW.InsertAsync("Payment", payment);
// 6. Update order status
order.Status = "Confirmed";
await _multiUoW.UpdateAsync("Order", order);
// 7. Commit all changes across all data sources
var result = await _multiUoW.CommitAsync();
if (result.Flag == Errors.Ok)
{
Console.WriteLine("β
Order processed successfully across all systems:");
Console.WriteLine($" π¦ Order: {order.OrderID}");
Console.WriteLine($" Γ°ΕΈβΒ° Total: ${orderTotal:F2}");
Console.WriteLine($" Γ°ΕΈΒΒͺ Items: {request.Items.Count}");
Console.WriteLine($" π― Customer: {customer.Name}");
return order.OrderID;
}
else
{
throw new BusinessException($"Order processing failed: {result.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"β Order processing failed: {ex.Message}");
Console.WriteLine("π All changes have been rolled back automatically");
throw;
}
}
public async Task<CustomerOrderSummary> GetCustomerOrderSummaryAsync(string customerID)
{
// Navigate to the customer
var customers = _multiUoW.GetEntities<Customer>("Customer");
var customer = customers.FirstOrDefault(c => c.CustomerID == customerID);
if (customer == null)
{
throw new ArgumentException("Customer not found");
}
// Navigate to load all related data
await _multiUoW.NavigateToParentAsync("Customer", customer);
// Get all related children
var relatedData = await _multiUoW.GetAllRelatedChildrenAsync<object, Customer>("Customer", customer);
var summary = new CustomerOrderSummary
{
Customer = customer,
OrderCount = relatedData.ContainsKey("Order") ? relatedData["Order"].Count : 0,
AddressCount = relatedData.ContainsKey("Address") ? relatedData["Address"].Count : 0
};
if (relatedData.ContainsKey("Order"))
{
var orders = relatedData["Order"].Cast<Order>();
summary.TotalSpent = orders.Sum(o => o.Total);
summary.LastOrderDate = orders.Max(o => o.OrderDate);
}
return summary;
}
private async Task UpdateCustomerOrderSummary()
{
// Business logic for updating customer metrics
Console.WriteLine("π Updating customer order summary...");
}
private async Task ValidateShippingOptions()
{
// Business logic for shipping validation
Console.WriteLine("Γ°ΕΈΕ‘Ε‘ Validating shipping options...");
}
private async Task UpdateOrderStatus()
{
// Business logic for order status updates
Console.WriteLine("π Updating order status...");
}
public void Dispose()
{
_multiUoW?.Dispose();
}
}
// Usage
public class OrderRequest
{
public string CustomerID { get; set; }
public string ShippingAddress { get; set; }
public string PaymentMethod { get; set; }
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
}
public class OrderItem
{
public string ProductID { get; set; }
public int Quantity { get; set; }
}
public class CustomerOrderSummary
{
public Customer Customer { get; set; }
public int OrderCount { get; set; }
public int AddressCount { get; set; }
public decimal TotalSpent { get; set; }
public DateTime? LastOrderDate { get; set; }
}
Best Practices
ποΈ Setup Phase
- Define all entities and relationships before starting operations
- Use meaningful entity names that match your domain
- Consider relationship behaviors carefully for each business rule
- Test relationship constraints in development
β‘ Performance
- Use specific generic types when possible
- Leverage caching of entity collections
- Consider pagination for large datasets
- Monitor cross-datasource query performance
π‘οΈ Error Handling
- Always wrap commits in try-catch blocks
- Handle relationship constraint violations gracefully
- Provide meaningful error messages to users
- Log detailed error information for debugging
π§ Resource Management
- Use
usingstatements for automatic disposal - Subscribe to events appropriately
- Unsubscribe from events when disposing
- Monitor memory usage with large datasets
- Missing Relationships: Not defining relationships can lead to data inconsistencies
- Wrong Behaviors: Using CascadeDelete inappropriately can cause unintended data loss
- Performance Issues: Loading too much related data at once
- Transaction Scope: Making changes outside the UoW that aren't part of the transaction
- Event Leaks: Not unsubscribing from events can cause memory leaks
- All data sources are properly configured in DME
- Entity relationships are clearly defined and tested
- Business rules are implemented through relationship behaviors
- Error handling is comprehensive and user-friendly
- Performance has been tested with realistic data volumes
- UI responds appropriately to data change events