Best Practices
Guidelines for effective use of Beep Data Management Engine components
β Table of Contents
General Principles
Design your data access layer following SOLID principles. Each UnitOfWork should have a single responsibility, and dependencies should be injected rather than created directly.
UnitOfWork implements IDisposable. Always use using statements or explicitly call Dispose() to ensure proper resource cleanup.
Proper Resource Management
// Good - Using statement ensures disposal
using (var uow = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID"))
{
// Work with UnitOfWork
await uow.Get();
uow.New();
// ... work with data
await uow.CommitAsync();
} // Automatically disposed here
// Also Good - Explicit disposal
var uow = new UnitOfWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID");
try
{
// Work with UnitOfWork
await uow.Get();
await uow.CommitAsync();
}
finally
{
uow.Dispose();
}
Always use async versions of methods for database operations to avoid blocking the UI thread.
Async Best Practices
// Good - Proper async usage
public async Task ProcessCustomerAsync(Customer customer)
{
await customerUoW.InsertAsync(customer);
var result = await customerUoW.CommitAsync();
if (result.Flag == Errors.Ok)
{
await NotifySuccess();
}
}
// Bad - Don't block async calls
public void ProcessCustomer(Customer customer)
{
customerUoW.InsertAsync(customer).Wait(); // Can cause deadlocks
var result = customerUoW.CommitAsync().Result; // Can cause deadlocks
}
UnitOfWork Best Practices
Initialize UnitOfWork instances early in your application lifecycle and reuse them throughout the session.
Service Pattern Implementation
public class CustomerService : IDisposable
{
private readonly UnitofWork<Customer> _customerUoW;
private readonly IDMEEditor _dmeEditor;
public CustomerService(IDMEEditor dmeEditor)
{
_dmeEditor = dmeEditor;
_customerUoW = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID");
// Setup event handlers once
_customerUoW.PreInsert += ValidateCustomer;
_customerUoW.PostEdit += LogChanges;
_customerUoW.PreDelete += CheckDeletePermission;
}
public async Task<List<Customer>> GetActiveCustomersAsync()
{
var filters = new List<AppFilter>
{
new AppFilter {FieldName = "IsActive", Operator = "=", FilterValue = true }
};
var result = await _customerUoW.Get(filters);
return result.ToList();
}
public async Task<bool> CreateCustomerAsync(Customer customer)
{
try
{
await _customerUoW.InsertAsync(customer);
var result = await _customerUoW.CommitAsync();
return result.Flag == Errors.Ok;
}
catch (Exception ex)
{
await _customerUoW.Rollback();
throw;
}
}
public void Dispose()
{
_customerUoW?.Dispose();
}
}
Use event handlers to implement validation logic at appropriate lifecycle points.
Validation Event Handlers
private void ValidateCustomer(object sender, UnitofWorkParams args)
{
var customer = args.CurrentEntity as Customer;
// Basic validation
if (string.IsNullOrWhiteSpace(customer.Email))
{
args.Cancel = true;
args.ErrorMessage = "Email is required";
return;
}
// Format validation
if (!IsValidEmail(customer.Email))
{
args.Cancel = true;
args.ErrorMessage = "Invalid email format";
return;
}
// Business rule validation
if (customer.CreditLimit > 100000 && customer.CustomerType != "Premium")
{
args.Cancel = true;
args.ErrorMessage = "High credit limits require Premium customer type";
return;
}
}
private void LogChanges(object sender, UnitofWorkParams args)
{
var customer = args.CurrentEntity as Customer;
_logger.LogInformation($"Customer {customer.CustomerID} modified: {args.PropertyName} = {args.PropertyValue}");
// Optional: Track changes for audit
if (args.PropertyName != null)
{
_auditService.LogChange(customer.CustomerID, args.PropertyName, args.PropertyValue);
}
}
Always check IsDirty before committing or closing to avoid data loss.
Unsaved Changes Handling
public async Task<bool> CloseFormAsync()
{
if (_customerUoW.IsDirty)
{
var result = MessageBox.Show(
"You have unsaved changes. Would you like to save them?",
"Unsaved Changes",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question);
switch (result)
{
case DialogResult.Yes:
var commitResult = await _customerUoW.CommitAsync();
if (commitResult.Flag != Errors.Ok)
{
MessageBox.Show($"Failed to save changes: {commitResult.Message}");
return false;
}
return true;
case DialogResult.No:
await _customerUoW.Rollback();
return true;
case DialogResult.Cancel:
return false;
default:
return false;
}
}
return true;
}
Implement proper rollback handling for error scenarios.
Transaction Rollback
public async Task<bool> ProcessBatchAsync(List<Customer> customers)
{
var addedCustomers = new List<Customer>();
try
{
foreach (var customer in customers)
{
// Validate before adding
ValidateCustomerData(customer);
await _customerUoW.InsertAsync(customer);
addedCustomers.Add(customer);
// Optional: Progress reporting
OnProgressUpdate?.Invoke(addedCustomers.Count, customers.Count);
}
var result = await _customerUoW.CommitAsync();
if (result.Flag != Errors.Ok)
{
await _customerUoW.Rollback();
return false;
}
return true;
}
catch (ValidationException ex)
{
await _customerUoW.Rollback();
_logger.LogWarning($"Batch processing failed due to validation: {ex.Message}");
throw;
}
catch (Exception ex)
{
await _customerUoW.Rollback();
_logger.LogError(ex, "Batch processing failed unexpectedly");
return false;
}
}
MultiDataSourceUnitOfWork Best Practices
Define all relationships before starting data operations to ensure proper coordination.
Relationship Configuration
private async Task InitializeMultiUoWAsync()
{
_multiUoW = new MultiDataSourceUnitOfWork(_dmeEditor);
// Add all entity types first
await _multiUoW.AddUnitOfWorkAsync<Customer>("SqlServer", "Customers", "CustomerID");
await _multiUoW.AddUnitOfWorkAsync<Order>("MongoDB", "Orders", "OrderID");
await _multiUoW.AddUnitOfWorkAsync<Address>("SqlServer", "Addresses", "AddressID");
await _multiUoW.AddUnitOfWorkAsync<AuditLog>("DocumentDB", "AuditLogs", "LogID");
// Define relationships with appropriate behaviors
_multiUoW.AddRelationship("Customer", "Order", "CustomerID", "CustomerID",
RelationshipBehavior.Restrict, false);
_multiUoW.AddRelationship("Customer", "Address", "CustomerID", "CustomerID",
RelationshipBehavior.CascadeDelete, false);
_multiUoW.AddRelationship("Order", "AuditLog", "OrderID", "EntityID",
RelationshipBehavior.CascadeDelete, true);
// Setup event handlers
_multiUoW.ChildDataChanged += OnChildDataChanged;
// Load initial data
await LoadInitialDataAsync();
}
private async Task LoadInitialDataAsync()
{
// Load customers first (parent entities)
var customers = _multiUoW.GetEntities<Customer>("Customer");
await customers.LoadAsync();
// Then load related data
if (customers.Any())
{
var firstCustomer = customers.First();
await _multiUoW.NavigateToParentAsync("Customer", firstCustomer);
}
}
Subscribe to ChildDataChanged events to keep UI synchronized with data changes.
Event-Driven UI Updates
private async void OnChildDataChanged(object sender, ChildDataChangedEventArgs args)
{
// Update UI based on which child data changed
switch (args.ChildEntityName)
{
case "Order":
await RefreshOrdersGrid();
UpdateOrderSummary();
break;
case "Address":
await RefreshAddressControls();
break;
case "AuditLog":
// Don't refresh UI for audit logs to avoid performance issues
break;
}
// Update any aggregate displays
UpdateCustomerSummary();
}
private async Task RefreshOrdersGrid()
{
var currentCustomer = _multiUoW.CurrentParentEntity as Customer;
if (currentCustomer != null)
{
var orders = await _multiUoW.GetAllRelatedChildrenAsync<Order, Customer>("Customer", currentCustomer);
// Update data binding
ordersDataGridView.DataSource = orders["Order"];
// Update status
lblOrderCount.Text = $"Orders: {orders["Order"].Count}";
}
}
Select relationship behaviors based on business requirements, not technical convenience.
Relationship Behavior Selection
// For optional relationships (customer notes)
_multiUoW.AddRelationship("Customer", "Note", "CustomerID", "CustomerID",
RelationshipBehavior.SetNull, false);
// For required audit trails (prevent deletion if history exists)
_multiUoW.AddRelationship("Order", "OrderHistory", "OrderID", "OrderID",
RelationshipBehavior.Restrict, true);
// For true parent-child relationships (invoice lines)
_multiUoW.AddRelationship("Invoice", "InvoiceLine", "InvoiceID", "InvoiceID",
RelationshipBehavior.CascadeDelete, true);
// For lookup relationships (don't delete customer if orders exist)
_multiUoW.AddRelationship("Customer", "Order", "CustomerID", "CustomerID",
RelationshipBehavior.Restrict, false);
Performance Optimization
Apply filters at the database level rather than in memory to improve performance.
Efficient Filtering
// Good - Database filtering
public async Task<List<Customer>> GetActiveCustomersAsync(DateTime since)
{
var filters = new List<AppFilter>
{
new AppFilter {FieldName = "IsActive", Operator = "=", FilterValue = true },
new AppFilter {FieldName = "LastOrderDate", Operator = ">=", FilterValue = since },
new AppFilter {FieldName = "CustomerType", Operator = "IN", FilterValue = new[] { "Premium", "Gold" } }
};
var result = await _customerUoW.Get(filters);
return result.ToList();
}
// Bad - In-memory filtering (loads all customers first)
public async Task<List<Customer>> GetActiveCustomersSlow(DateTime since)
{
var allCustomers = await _customerUoW.Get(); // Loads everything!
return allCustomers
.Where(c => c.IsActive &&
c.LastOrderDate >= since &&
(c.CustomerType == "Premium" || c.CustomerType == "Gold"))
.ToList();
}
For large datasets, process data in batches to avoid memory issues.
Batch Processing
public async Task ProcessLargeDatasetAsync(IEnumerable<ImportData> data,
IProgress<int> progress = null)
{
const int batchSize = 1000;
var batch = new List<Customer>();
var totalProcessed = 0;
foreach (var item in data)
{
var customer = CreateCustomerFromImport(item);
batch.Add(customer);
if (batch.Count >= batchSize)
{
await ProcessBatchAsync(batch);
totalProcessed += batch.Count;
progress?.Report(totalProcessed);
batch.Clear();
// Optional: Allow for cancellation and cleanup
if (_cancellationToken.IsCancellationRequested)
{
break;
}
// Optional: Brief pause to prevent overwhelming the database
await Task.Delay(10, _cancellationToken);
}
}
// Process remaining items
if (batch.Any())
{
await ProcessBatchAsync(batch);
totalProcessed += batch.Count;
progress?.Report(totalProcessed);
}
}
private async Task ProcessBatchAsync(List<Customer> batch)
{
try
{
foreach (var customer in batch)
{
await _customerUoW.InsertAsync(customer);
}
await _customerUoW.CommitAsync();
}
catch
{
await _customerUoW.Rollback();
throw;
}
}
Implement progress reporting for operations that might take significant time.
Progress Reporting
public async Task ImportDataWithProgressAsync(List<ImportData> data)
{
var progress = new Progress<PassedArgs>(args =>
{
// Update progress bar
progressBar.Value = args.ParameterInt1;
progressBar.Maximum = args.ParameterInt2;
// Update status label
lblStatus.Text = args.ParameterString1;
// Allow UI to refresh
Application.DoEvents();
});
var cancellationTokenSource = new CancellationTokenSource();
btnCancel.Click += (s, e) => cancellationTokenSource.Cancel();
try
{
var result = await _customerUoW.Commit(progress, cancellationTokenSource.Token);
if (result.Flag == Errors.Ok)
{
MessageBox.Show("Import completed successfully!");
}
}
catch (OperationCanceledException)
{
MessageBox.Show("Import was cancelled.");
await _customerUoU.Rollback();
}
}
Error Handling Best Practices
Handle different types of errors appropriately and provide meaningful feedback.
Comprehensive Error Handling
public async Task<ServiceResult> CreateCustomerAsync(CustomerData data)
{
try
{
var customer = MapToEntity(data);
// Pre-validation
ValidateCustomerData(customer);
await _customerUoW.InsertAsync(customer);
var result = await _customerUoW.CommitAsync();
if (result.Flag == Errors.Ok)
{
_logger.LogInformation($"Customer created successfully: {customer.CustomerID}");
return ServiceResult.Success(customer.CustomerID);
}
else
{
_logger.LogWarning($"Database error creating customer: {result.Message}");
return ServiceResult.Failure($"Database error: {result.Message}");
}
}
catch (ValidationException ex)
{
_logger.LogWarning($"Validation error creating customer: {ex.Message}");
return ServiceResult.ValidationError(ex.Message);
}
catch (SqlException ex) when (ex.Number == 2) // Timeout
{
_logger.LogError(ex, "Database timeout creating customer");
return ServiceResult.Failure("Database timeout. Please try again later.");
}
catch (SqlException ex) when (ex.Number == 2627) // Duplicate key
{
_logger.LogWarning($"Duplicate customer creation attempt: {ex.Message}");
return ServiceResult.Failure("A customer with this information already exists.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error creating customer");
return ServiceResult.Failure("An unexpected error occurred. Please contact support.");
}
}
Enable logging for audit trails and debugging purposes.
Audit Logging
public void ConfigureLogging()
{
// Enable logging
_customerUoW.IsLogging = true;
// Setup periodic log saving
_logTimer = new Timer(SaveLogsToFile, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
private void SaveLogsToFile(object state)
{
try
{
var logPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp", "Logs", $"customer-changes-{DateTime.Now:yyyyMMdd}.json");
Directory.CreateDirectory(Path.GetDirectoryName(logPath));
_customerUoW.SaveLog(logPath);
_logger.LogDebug($"Change log saved to {logPath}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save change log");
}
}
Testing Strategies
Use list mode for unit testing to avoid database dependencies.
Unit Testing with List Mode
[TestFixture]
public class CustomerUnitOfWorkTests
{
private UnitofWork<Customer> _uow;
private ObservableBindingList<Customer> _testData;
private Mock<IDMEEditor> _mockDMEEditor;
[SetUp]
public void Setup()
{
_mockDMEEditor = new Mock<IDMEEditor>();
_testData = new ObservableBindingList<Customer>();
// Create UnitOfWork in list mode (no database)
_uow = new UnitofWork<Customer>(_mockDMEEditor.Object, true, _testData, "CustomerID");
}
[Test]
public void Should_Track_New_Entity_As_Added()
{
// Arrange & Act
_uow.New();
var customer = _uow.CurrentItem;
customer.Name = "Test Customer";
customer.Email = "test@example.com";
// Assert
Assert.IsTrue(_uow.IsDirty);
Assert.AreEqual(1, _uow.GetAddedEntities().Count());
Assert.AreEqual("Test Customer", customer.Name);
}
[Test]
public void Should_Track_Modified_Entity()
{
// Arrange
var existingCustomer = new Customer { CustomerID = 1, Name = "Original Name" };
_testData.Add(existingCustomer);
_uow.MoveTo(0);
// Act
_uow.CurrentItem.Name = "Modified Name";
// Assert
Assert.IsTrue(_uow.IsDirty);
Assert.AreEqual(1, _uow.GetModifiedEntities().Count());
}
[Test]
public void Should_Track_Deleted_Entity()
{
// Arrange
var customerToDelete = new Customer { CustomerID = 1, Name = "To Delete" };
_testData.Add(customerToDelete);
// Act
_uow.Delete(customerToDelete);
// Assert
Assert.IsTrue(_uow.IsDirty);
Assert.AreEqual(1, _uow.GetDeletedEntities().Count());
Assert.AreEqual(0, _testData.Count);
}
[TearDown]
public void TearDown()
{
_uow?.Dispose();
}
}
Mock IDMEEditor and IDataSource for isolated unit testing.
Mocking Dependencies
[TestFixture]
public class CustomerServiceTests
{
private CustomerService _customerService;
private Mock<IDMEEditor> _mockDMEEditor;
private Mock<IDataSource> _mockDataSource;
private Mock<ILogger> _mockLogger;
[SetUp]
public void Setup()
{
_mockDMEEditor = new Mock<IDMEEditor>();
_mockDataSource = new Mock<IDataSource>();
_mockLogger = new Mock<ILogger>();
// Setup mock returns
_mockDMEEditor.Setup(x => x.GetDataSource(It.IsAny<string>()))
.Returns(_mockDataSource.Object);
_mockDataSource.Setup(x => x.GetEntity(It.IsAny<string>(), It.IsAny<List<AppFilter>>()))
.Returns(new List<Customer>());
_mockDataSource.Setup(x => x.InsertEntity(It.IsAny<string>(), It.IsAny<object>()))
.Returns(new ErrorsInfo { Flag = Errors.Ok });
_customerService = new CustomerService(_mockDMEEditor.Object, _mockLogger.Object);
}
[Test]
public async Task Should_Create_Customer_Successfully()
{
// Arrange
var customer = new Customer { Name = "Test Customer", Email = "test@example.com" };
// Act
var result = await _customerService.CreateCustomerAsync(customer);
// Assert
Assert.IsTrue(result.IsSuccess);
_mockDataSource.Verify(x => x.InsertEntity("Customers", It.IsAny<object>()), Times.Once);
}
[Test]
public async Task Should_Handle_Validation_Errors()
{
// Arrange
var invalidCustomer = new Customer { Name = "", Email = "invalid-email" };
// Act & Assert
var exception = await Assert.ThrowsAsync<ValidationException>(
() => _customerService.CreateCustomerAsync(invalidCustomer));
Assert.That(exception.Message, Contains.Substring("Name is required"));
}
}
Security Guidelines
Always validate input data before processing to prevent injection attacks and data corruption.
Input Validation
private void ValidateCustomerInput(Customer customer)
{
// Required field validation
if (string.IsNullOrWhiteSpace(customer.Name))
throw new ValidationException("Name is required");
// Length validation
if (customer.Name.Length > 100)
throw new ValidationException("Name cannot exceed 100 characters");
// Format validation
if (!string.IsNullOrEmpty(customer.Email) && !IsValidEmail(customer.Email))
throw new ValidationException("Invalid email format");
// SQL injection prevention (if building dynamic queries)
if (ContainsSqlInjectionPatterns(customer.Name))
throw new ValidationException("Invalid characters detected");
// XSS prevention
customer.Name = HttpUtility.HtmlEncode(customer.Name?.Trim());
customer.Email = customer.Email?.Trim().ToLowerInvariant();
}
private bool IsValidEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
private bool ContainsSqlInjectionPatterns(string input)
{
var patterns = new[] { "--", ";", "/*", "*/", "xp_", "sp_", "exec", "execute" };
return patterns.Any(pattern => input.ToLower().Contains(pattern));
}
Check user permissions before allowing data operations.
Authorization Implementation
private void CheckPermissions(string operation, Customer customer)
{
var currentUser = _securityService.GetCurrentUser();
// Check general permissions
if (!currentUser.HasPermission($"Customer.{operation}"))
{
throw new UnauthorizedAccessException($"User not authorized for {operation} operation");
}
// Check record-level security
switch (operation)
{
case "Delete":
if (customer.CreatedBy != currentUser.UserId && !currentUser.HasRole("Admin"))
{
throw new UnauthorizedAccessException("Can only delete own records");
}
break;
case "Update":
if (customer.IsLocked && !currentUser.HasRole("Admin"))
{
throw new UnauthorizedAccessException("Cannot modify locked records");
}
break;
case "View":
if (customer.IsConfidential && !currentUser.HasPermission("Customer.ViewConfidential"))
{
throw new UnauthorizedAccessException("Cannot view confidential records");
}
break;
}
// Log access for audit
_auditService.LogAccess(currentUser.UserId, operation, customer.CustomerID);
}
Common Anti-Patterns to Avoid
Creating new UnitOfWork instances in loops is inefficient and can cause resource issues.
Loop Anti-Pattern
// BAD - Don't do this
public async Task ImportCustomersBadAsync(List<ImportData> importData)
{
foreach (var customerData in importData)
{
// Creates new connection each time - VERY INEFFICIENT
using (var uow = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID"))
{
await uow.InsertAsync(CreateCustomer(customerData));
await uow.CommitAsync(); // Individual commits - slow and not transactional
}
}
}
// GOOD - Reuse UnitOfWork
public async Task ImportCustomersGoodAsync(List<ImportData> importData)
{
using (var uow = new UnitofWork<Customer>(dmeEditor, "SqlServer", "Customers", "CustomerID"))
{
foreach (var customerData in importData)
{
var customer = CreateCustomer(customerData);
await uow.InsertAsync(customer);
}
// Single commit for all operations - faster and transactional
await uow.CommitAsync();
}
}
Always respect the Cancel flag in event handlers.
Event Cancellation Anti-Pattern
// BAD - Ignoring cancellation
private void OnPreInsert_Bad(object sender, UnitofWorkParams args)
{
var customer = args.CurrentEntity as Customer;
if (customer.Email == null)
{
args.Cancel = true; // Set cancel but continue processing
customer.Email = "unknown@example.com"; // This shouldn't happen after cancel
// Continue with other operations that should be skipped
ValidateOtherFields(customer);
}
}
// GOOD - Respect cancellation
private void OnPreInsert_Good(object sender, UnitofWorkParams args)
{
var customer = args.CurrentEntity as Customer;
if (customer.Email == null)
{
args.Cancel = true;
args.ErrorMessage = "Email is required";
return; // Stop processing immediately
}
// Only continue if not cancelled
ValidateOtherFields(customer);
}
Mixing sync and async calls can lead to deadlocks and performance issues.
Async/Sync Mixing Anti-Pattern
// BAD - Blocking async calls (can cause deadlocks)
public void ProcessCustomer_Bad(Customer customer)
{
try
{
_customerUoW.InsertAsync(customer).Wait(); // DANGEROUS!
var result = _customerUoW.CommitAsync().Result; // DANGEROUS!
if (result.Flag == Errors.Ok)
{
UpdateUI(); // May never be reached due to deadlock
}
}
catch (AggregateException ex)
{
// Have to unwrap aggregate exceptions
HandleException(ex.InnerException);
}
}
// GOOD - Async all the way
public async Task ProcessCustomer_Good(Customer customer)
{
try
{
await _customerUoW.InsertAsync(customer);
var result = await _customerUoW.CommitAsync();
if (result.Flag == Errors.Ok)
{
await UpdateUIAsync();
}
}
catch (Exception ex)
{
// Direct exception handling
HandleException(ex);
}
}
Deployment and Production Checklist
Ensure these items are verified before deploying to production:
π§ Configuration
- β Connection timeouts configured appropriately
- β Batch sizes optimized for data volume
- β Relationship behaviors documented
- β Environment-specific settings applied
π Logging & Monitoring
- β Logging levels configured correctly
- β Error alerting system in place
- β Performance monitoring enabled
- β Audit trail configuration verified
π§ͺ Testing
- β Load testing completed
- β Rollback scenarios tested
- β Error handling verified
- β Resource disposal confirmed
π Security
- β Input validation implemented
- β Authorization checks in place
- β SQL injection prevention verified
- β Sensitive data protection confirmed
π₯ Health Checks
- β Data source health monitoring
- β Connection pool monitoring
- β Memory usage tracking
- β Performance baseline established
π Documentation
- β Support procedures documented
- β Common error scenarios documented
- β Backup/recovery procedures ready
- β Troubleshooting guide available
These best practices are guidelines based on common scenarios. Always adapt them to your specific requirements and constraints. Regular code reviews and performance testing will help ensure your implementation remains optimal as your application evolves.