1177 lines
38 KiB
Markdown
1177 lines
38 KiB
Markdown
# Performance Patterns and Optimization - Centron Enterprise Application
|
|
|
|
## Overview
|
|
|
|
This document provides comprehensive coverage of performance patterns and optimizations within the Centron .NET 8 enterprise application. Performance patterns encompass caching strategies, query optimization, lazy loading, asynchronous programming, resource management, and monitoring techniques that ensure optimal application performance under various load conditions.
|
|
|
|
## Performance Architecture
|
|
|
|
### **Performance Design Principles**
|
|
|
|
The Centron application implements performance optimization through multiple strategies:
|
|
|
|
1. **Caching**: Multi-level caching from database to UI components
|
|
2. **Asynchronous Processing**: Non-blocking I/O operations and background tasks
|
|
3. **Query Optimization**: Efficient database queries with proper indexing
|
|
4. **Resource Management**: Proper disposal and pooling of expensive resources
|
|
5. **Lazy Loading**: On-demand data loading to reduce initial load times
|
|
6. **Background Processing**: Offload heavy operations to background services
|
|
|
|
## Caching Patterns
|
|
|
|
### **1. Multi-Level Caching Strategy**
|
|
|
|
```csharp
|
|
public interface ICacheService
|
|
{
|
|
Task<Result<T>> GetAsync<T>(string key) where T : class;
|
|
Task<Result> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class;
|
|
Task<Result> RemoveAsync(string key);
|
|
Task<Result> RemoveByPatternAsync(string pattern);
|
|
Task<Result> ClearAsync();
|
|
}
|
|
|
|
public class HybridCacheService : ICacheService
|
|
{
|
|
private readonly IMemoryCache _memoryCache;
|
|
private readonly IDistributedCache _distributedCache;
|
|
private readonly ILogger<HybridCacheService> _logger;
|
|
private readonly CacheConfiguration _config;
|
|
|
|
public HybridCacheService(
|
|
IMemoryCache memoryCache,
|
|
IDistributedCache distributedCache,
|
|
CacheConfiguration config)
|
|
{
|
|
_memoryCache = memoryCache;
|
|
_distributedCache = distributedCache;
|
|
_config = config;
|
|
}
|
|
|
|
public async Task<Result<T>> GetAsync<T>(string key) where T : class
|
|
{
|
|
try
|
|
{
|
|
// Level 1: Memory Cache (fastest)
|
|
if (_memoryCache.TryGetValue(key, out T cachedValue))
|
|
{
|
|
return Result<T>.AsSuccess(cachedValue);
|
|
}
|
|
|
|
// Level 2: Distributed Cache (Redis/SQL Server)
|
|
var distributedValue = await _distributedCache.GetStringAsync(key);
|
|
if (!string.IsNullOrEmpty(distributedValue))
|
|
{
|
|
var deserializedValue = JsonSerializer.Deserialize<T>(distributedValue);
|
|
|
|
// Store in memory cache for faster subsequent access
|
|
var memoryExpiry = TimeSpan.FromMinutes(_config.MemoryCacheMinutes);
|
|
_memoryCache.Set(key, deserializedValue, memoryExpiry);
|
|
|
|
return Result<T>.AsSuccess(deserializedValue);
|
|
}
|
|
|
|
return Result<T>.AsError("Cache miss");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Cache get operation failed for key: {Key}", key);
|
|
return Result<T>.AsError($"Cache operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
|
|
{
|
|
try
|
|
{
|
|
var cacheExpiry = expiry ?? TimeSpan.FromMinutes(_config.DefaultExpiryMinutes);
|
|
|
|
// Set in memory cache
|
|
_memoryCache.Set(key, value, TimeSpan.FromMinutes(_config.MemoryCacheMinutes));
|
|
|
|
// Set in distributed cache
|
|
var serializedValue = JsonSerializer.Serialize(value);
|
|
var options = new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = cacheExpiry
|
|
};
|
|
|
|
await _distributedCache.SetStringAsync(key, serializedValue, options);
|
|
|
|
return Result.AsSuccess();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Cache set operation failed for key: {Key}", key);
|
|
return Result.AsError($"Cache operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache-Aside Pattern Implementation
|
|
public class CachedAccountRepository : IAccountRepository
|
|
{
|
|
private readonly IAccountRepository _baseRepository;
|
|
private readonly ICacheService _cacheService;
|
|
private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(15);
|
|
|
|
public CachedAccountRepository(IAccountRepository baseRepository, ICacheService cacheService)
|
|
{
|
|
_baseRepository = baseRepository;
|
|
_cacheService = cacheService;
|
|
}
|
|
|
|
public async Task<Result<Account>> GetAccountByIdAsync(int accountId)
|
|
{
|
|
var cacheKey = $"account:{accountId}";
|
|
|
|
// Try cache first
|
|
var cachedResult = await _cacheService.GetAsync<Account>(cacheKey);
|
|
if (cachedResult.Status == ResultStatus.Success)
|
|
return cachedResult;
|
|
|
|
// Cache miss - get from database
|
|
var dbResult = await _baseRepository.GetAccountByIdAsync(accountId);
|
|
if (dbResult.Status == ResultStatus.Success)
|
|
{
|
|
// Cache the result
|
|
await _cacheService.SetAsync(cacheKey, dbResult.Data, _cacheExpiry);
|
|
}
|
|
|
|
return dbResult;
|
|
}
|
|
|
|
public async Task<Result<Account>> SaveAccountAsync(Account account)
|
|
{
|
|
var result = await _baseRepository.SaveAccountAsync(account);
|
|
if (result.Status == ResultStatus.Success)
|
|
{
|
|
// Update cache with new data
|
|
var cacheKey = $"account:{account.I3D}";
|
|
await _cacheService.SetAsync(cacheKey, result.Data, _cacheExpiry);
|
|
|
|
// Invalidate related cached data
|
|
await InvalidateRelatedCacheAsync(account);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task InvalidateRelatedCacheAsync(Account account)
|
|
{
|
|
// Remove account lists that might include this account
|
|
await _cacheService.RemoveByPatternAsync("accounts:list:*");
|
|
await _cacheService.RemoveByPatternAsync($"customer:{account.CustomerNumber}:*");
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Application-Level Caching Patterns**
|
|
|
|
```csharp
|
|
public class CachedTableService
|
|
{
|
|
private readonly ICacheService _cacheService;
|
|
private readonly ICachedTableRepository _repository;
|
|
|
|
public async Task<Result<List<T>>> GetCachedDataAsync<T>(string tableName) where T : class
|
|
{
|
|
var cacheKey = $"cached_table:{tableName}";
|
|
|
|
var cachedResult = await _cacheService.GetAsync<CachedTableData<T>>(cacheKey);
|
|
if (cachedResult.Status == ResultStatus.Success)
|
|
{
|
|
var cachedData = cachedResult.Data;
|
|
|
|
// Check if cache needs refresh
|
|
var lastModified = await _repository.GetLastModifiedAsync(tableName);
|
|
if (lastModified <= cachedData.CacheTime)
|
|
{
|
|
return Result<List<T>>.AsSuccess(cachedData.Data);
|
|
}
|
|
}
|
|
|
|
// Cache is stale or missing - refresh from database
|
|
return await RefreshCachedDataAsync<T>(tableName);
|
|
}
|
|
|
|
private async Task<Result<List<T>>> RefreshCachedDataAsync<T>(string tableName) where T : class
|
|
{
|
|
var dataResult = await _repository.GetDataAsync<T>(tableName);
|
|
if (dataResult.Status != ResultStatus.Success)
|
|
return dataResult;
|
|
|
|
var cachedData = new CachedTableData<T>
|
|
{
|
|
Data = dataResult.Data,
|
|
CacheTime = DateTime.UtcNow,
|
|
TableName = tableName
|
|
};
|
|
|
|
// Cache with appropriate expiry based on table type
|
|
var expiry = GetCacheExpiryForTable(tableName);
|
|
await _cacheService.SetAsync($"cached_table:{tableName}", cachedData, expiry);
|
|
|
|
return Result<List<T>>.AsSuccess(cachedData.Data);
|
|
}
|
|
|
|
private TimeSpan GetCacheExpiryForTable(string tableName)
|
|
{
|
|
return tableName.ToLower() switch
|
|
{
|
|
"countries" => TimeSpan.FromHours(24), // Static reference data
|
|
"currencies" => TimeSpan.FromHours(12),
|
|
"products" => TimeSpan.FromMinutes(30), // More dynamic data
|
|
"prices" => TimeSpan.FromMinutes(5), // Frequently changing
|
|
_ => TimeSpan.FromHours(1) // Default
|
|
};
|
|
}
|
|
}
|
|
|
|
// Background cache warming service
|
|
public class CacheWarmupService : BackgroundService
|
|
{
|
|
private readonly ICachedTableService _cachedTableService;
|
|
private readonly ILogger<CacheWarmupService> _logger;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await WarmupCriticalCaches();
|
|
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error during cache warmup");
|
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task WarmupCriticalCaches()
|
|
{
|
|
var criticalTables = new[] { "countries", "currencies", "taxrates", "paymentmethods" };
|
|
|
|
var warmupTasks = criticalTables.Select(async table =>
|
|
{
|
|
try
|
|
{
|
|
await _cachedTableService.GetCachedDataAsync<object>(table);
|
|
_logger.LogDebug("Warmed up cache for table: {Table}", table);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to warm up cache for table: {Table}", table);
|
|
}
|
|
});
|
|
|
|
await Task.WhenAll(warmupTasks);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Asynchronous Programming Patterns
|
|
|
|
### **1. Async/Await Best Practices**
|
|
|
|
```csharp
|
|
public class OptimizedBusinessLogic : BaseBL
|
|
{
|
|
// Correct async pattern with ConfigureAwait
|
|
public async Task<Result<List<Account>>> GetAccountsAsync(AccountFilter filter)
|
|
{
|
|
try
|
|
{
|
|
// Use ConfigureAwait(false) in library code to avoid deadlocks
|
|
var accounts = await _accountRepository
|
|
.GetAccountsAsync(filter)
|
|
.ConfigureAwait(false);
|
|
|
|
return Result<List<Account>>.AsSuccess(accounts);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<List<Account>>.AsError($"Failed to get accounts: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Parallel processing for independent operations
|
|
public async Task<Result<AccountSummary>> GetAccountSummaryAsync(int accountId)
|
|
{
|
|
try
|
|
{
|
|
// Start all async operations concurrently
|
|
var accountTask = _accountRepository.GetAccountByIdAsync(accountId);
|
|
var contractsTask = _contractRepository.GetContractsByAccountAsync(accountId);
|
|
var invoicesTask = _invoiceRepository.GetRecentInvoicesAsync(accountId);
|
|
var paymentsTask = _paymentRepository.GetRecentPaymentsAsync(accountId);
|
|
|
|
// Wait for all to complete
|
|
await Task.WhenAll(accountTask, contractsTask, invoicesTask, paymentsTask)
|
|
.ConfigureAwait(false);
|
|
|
|
// Process results
|
|
var account = await accountTask.ConfigureAwait(false);
|
|
var contracts = await contractsTask.ConfigureAwait(false);
|
|
var invoices = await invoicesTask.ConfigureAwait(false);
|
|
var payments = await paymentsTask.ConfigureAwait(false);
|
|
|
|
if (account.Status != ResultStatus.Success)
|
|
return Result<AccountSummary>.FromResult(account);
|
|
|
|
var summary = new AccountSummary
|
|
{
|
|
Account = account.Data,
|
|
ActiveContracts = contracts.Data?.Where(c => c.IsActive).ToList() ?? new List<Contract>(),
|
|
RecentInvoices = invoices.Data ?? new List<Invoice>(),
|
|
RecentPayments = payments.Data ?? new List<Payment>(),
|
|
TotalOutstanding = CalculateOutstanding(invoices.Data, payments.Data)
|
|
};
|
|
|
|
return Result<AccountSummary>.AsSuccess(summary);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<AccountSummary>.AsError($"Failed to get account summary: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Batch processing with controlled concurrency
|
|
public async Task<Result<BatchProcessResult>> ProcessAccountsBatchAsync(List<int> accountIds, int maxConcurrency = 10)
|
|
{
|
|
var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
|
var results = new ConcurrentBag<ProcessResult>();
|
|
var errors = new ConcurrentBag<string>();
|
|
|
|
var tasks = accountIds.Select(async accountId =>
|
|
{
|
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
var result = await ProcessSingleAccountAsync(accountId).ConfigureAwait(false);
|
|
results.Add(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add($"Account {accountId}: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
});
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
|
return Result<BatchProcessResult>.AsSuccess(new BatchProcessResult
|
|
{
|
|
SuccessfulResults = results.ToList(),
|
|
Errors = errors.ToList(),
|
|
TotalProcessed = accountIds.Count
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Background Task Processing Patterns**
|
|
|
|
```csharp
|
|
public abstract class ManagedBackgroundService : BackgroundService
|
|
{
|
|
private readonly ILogger _logger;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
protected abstract string ServiceName { get; }
|
|
protected abstract TimeSpan GetExecutionInterval();
|
|
protected abstract void ExecuteService(CancellationToken stoppingToken);
|
|
protected virtual void InitializeService(CancellationToken stoppingToken) { }
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("Starting background service: {ServiceName}", ServiceName);
|
|
|
|
try
|
|
{
|
|
InitializeService(stoppingToken);
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
using (var scope = _serviceProvider.CreateScope())
|
|
{
|
|
ExecuteService(stoppingToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error executing background service: {ServiceName}", ServiceName);
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
var executionTime = stopwatch.Elapsed;
|
|
var interval = GetExecutionInterval();
|
|
|
|
// Ensure we don't execute too frequently
|
|
var delay = interval - executionTime;
|
|
if (delay > TimeSpan.Zero)
|
|
{
|
|
await Task.Delay(delay, stoppingToken);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_logger.LogInformation("Background service stopped: {ServiceName}", ServiceName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Specialized cache update service
|
|
public class CacheUpdateService : ManagedBackgroundService
|
|
{
|
|
private DateTime? _lastOptimizedUpdate = null;
|
|
|
|
protected override string ServiceName => "CacheUpdateService";
|
|
|
|
protected override void ExecuteService(CancellationToken stoppingToken)
|
|
{
|
|
using (var session = new BLSession())
|
|
{
|
|
// Regular cache updates
|
|
session.GetBL<CachedTableBL>().ExecuteCacheUpdates().ThrowIfError();
|
|
|
|
// Optimized cache updates (less frequent)
|
|
if (_lastOptimizedUpdate == null || DateTime.Now - _lastOptimizedUpdate > TimeSpan.FromMinutes(1))
|
|
{
|
|
session.GetBL<CachedTableBL>().ExecuteOptimizedCacheUpdates().ThrowIfError();
|
|
_lastOptimizedUpdate = DateTime.Now;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override TimeSpan GetExecutionInterval()
|
|
{
|
|
return TimeSpan.FromSeconds(2); // High frequency for immediate updates
|
|
}
|
|
|
|
protected override void InitializeService(CancellationToken stoppingToken)
|
|
{
|
|
try
|
|
{
|
|
using var session = new BLSession();
|
|
session.GetBL<CachedTableBL>().RepairIfNecessaryCacheStatistic();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogError(e, "Failed to repair the cached statistic table");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Task queue background processor
|
|
public class TaskQueueProcessor : BackgroundService
|
|
{
|
|
private readonly ITaskQueue _taskQueue;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly ILogger<TaskQueueProcessor> _logger;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var task = await _taskQueue.DequeueAsync(stoppingToken);
|
|
|
|
// Process task in separate scope for proper DI cleanup
|
|
using (var scope = _serviceProvider.CreateScope())
|
|
{
|
|
await ProcessTaskAsync(task, scope.ServiceProvider);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error processing background task");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ProcessTaskAsync(BackgroundTask task, IServiceProvider serviceProvider)
|
|
{
|
|
try
|
|
{
|
|
task.Status = TaskStatus.Processing;
|
|
task.StartedAt = DateTime.UtcNow;
|
|
|
|
var processor = serviceProvider.GetRequiredService(task.ProcessorType) as ITaskProcessor;
|
|
var result = await processor.ProcessAsync(task.Data);
|
|
|
|
task.Status = result.Status == ResultStatus.Success ? TaskStatus.Completed : TaskStatus.Failed;
|
|
task.Result = result.Data;
|
|
task.Error = result.Error;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
task.Status = TaskStatus.Failed;
|
|
task.Error = ex.Message;
|
|
_logger.LogError(ex, "Task processing failed: {TaskId}", task.Id);
|
|
}
|
|
finally
|
|
{
|
|
task.CompletedAt = DateTime.UtcNow;
|
|
await _taskQueue.UpdateTaskAsync(task);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database Query Optimization Patterns
|
|
|
|
### **1. Efficient Query Patterns**
|
|
|
|
```csharp
|
|
public class OptimizedQueryRepository
|
|
{
|
|
private readonly ISession _session;
|
|
|
|
// Projection query - only select needed fields
|
|
public async Task<Result<List<AccountSummaryDto>>> GetAccountSummariesAsync(AccountFilter filter)
|
|
{
|
|
try
|
|
{
|
|
var query = _session.QueryOver<Account>()
|
|
.Where(a => a.IsActive)
|
|
.SelectList(list => list
|
|
.Select(a => a.I3D).WithAlias(() => alias.Id)
|
|
.Select(a => a.CompanyName).WithAlias(() => alias.CompanyName)
|
|
.Select(a => a.CustomerNumber).WithAlias(() => alias.CustomerNumber)
|
|
.Select(a => a.CreditLimit).WithAlias(() => alias.CreditLimit))
|
|
.TransformUsing(Transformers.AliasToBean<AccountSummaryDto>());
|
|
|
|
// Apply filters
|
|
if (filter.CustomerNumbers?.Any() == true)
|
|
query.WhereRestrictionOn(a => a.CustomerNumber).IsIn(filter.CustomerNumbers);
|
|
|
|
if (!string.IsNullOrEmpty(filter.CompanyNameFilter))
|
|
query.Where(Restrictions.InsensitiveLike("CompanyName", filter.CompanyNameFilter, MatchMode.Anywhere));
|
|
|
|
// Pagination
|
|
if (filter.PageSize > 0)
|
|
{
|
|
query.Skip(filter.Page * filter.PageSize).Take(filter.PageSize);
|
|
}
|
|
|
|
var results = await Task.FromResult(query.List<AccountSummaryDto>());
|
|
return Result<List<AccountSummaryDto>>.AsSuccess(results.ToList());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<List<AccountSummaryDto>>.AsError($"Query failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Batch loading with proper joins
|
|
public async Task<Result<List<Account>>> GetAccountsWithContactsAsync(List<int> accountIds)
|
|
{
|
|
try
|
|
{
|
|
// Use batch loading to avoid N+1 queries
|
|
var accounts = await Task.FromResult(
|
|
_session.QueryOver<Account>()
|
|
.Fetch(a => a.Contacts).Eager
|
|
.Fetch(a => a.Addresses).Eager
|
|
.WhereRestrictionOn(a => a.I3D).IsIn(accountIds)
|
|
.List());
|
|
|
|
return Result<List<Account>>.AsSuccess(accounts.ToList());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<List<Account>>.AsError($"Batch query failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Optimized aggregation query
|
|
public async Task<Result<AccountStatistics>> GetAccountStatisticsAsync()
|
|
{
|
|
try
|
|
{
|
|
var statistics = await Task.FromResult(
|
|
_session.QueryOver<Account>()
|
|
.SelectList(list => list
|
|
.SelectCount(a => a.I3D).WithAlias(() => alias.TotalAccounts)
|
|
.SelectSum(a => a.CreditLimit).WithAlias(() => alias.TotalCreditLimit)
|
|
.SelectAvg(a => a.CreditLimit).WithAlias(() => alias.AverageCreditLimit))
|
|
.Where(a => a.IsActive)
|
|
.TransformUsing(Transformers.AliasToBean<AccountStatistics>())
|
|
.SingleOrDefault<AccountStatistics>());
|
|
|
|
return Result<AccountStatistics>.AsSuccess(statistics);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<AccountStatistics>.AsError($"Statistics query failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lazy loading pattern for large datasets
|
|
public class LazyLoadingService
|
|
{
|
|
public async Task<Result<PagedResult<T>>> GetPagedDataAsync<T>(
|
|
IQueryable<T> baseQuery,
|
|
int page,
|
|
int pageSize,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var totalCount = await baseQuery.CountAsync(cancellationToken);
|
|
|
|
var items = await baseQuery
|
|
.Skip(page * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return Result<PagedResult<T>>.AsSuccess(new PagedResult<T>
|
|
{
|
|
Items = items,
|
|
TotalCount = totalCount,
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
TotalPages = (int)Math.Ceiling((double)totalCount / pageSize)
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<PagedResult<T>>.AsError($"Paged query failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Connection and Transaction Optimization**
|
|
|
|
```csharp
|
|
public class OptimizedDataAccess
|
|
{
|
|
// Connection pooling and session management
|
|
public class OptimizedSession : IDisposable
|
|
{
|
|
private readonly ISession _session;
|
|
private readonly ITransaction _transaction;
|
|
|
|
public OptimizedSession()
|
|
{
|
|
_session = SessionFactory.OpenSession();
|
|
_transaction = _session.BeginTransaction(IsolationLevel.ReadCommitted);
|
|
}
|
|
|
|
public async Task<Result<T>> ExecuteAsync<T>(Func<ISession, Task<T>> operation)
|
|
{
|
|
try
|
|
{
|
|
var result = await operation(_session);
|
|
await _transaction.CommitAsync();
|
|
return Result<T>.AsSuccess(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _transaction.RollbackAsync();
|
|
return Result<T>.AsError($"Operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_transaction?.Dispose();
|
|
_session?.Dispose();
|
|
}
|
|
}
|
|
|
|
// Batch operations for better performance
|
|
public async Task<Result> BatchInsertAsync<T>(IEnumerable<T> entities) where T : class
|
|
{
|
|
try
|
|
{
|
|
using var session = new OptimizedSession();
|
|
|
|
return await session.ExecuteAsync(async s =>
|
|
{
|
|
var batchSize = 100;
|
|
var batch = 0;
|
|
|
|
foreach (var entity in entities)
|
|
{
|
|
await s.SaveAsync(entity);
|
|
|
|
if (++batch % batchSize == 0)
|
|
{
|
|
await s.FlushAsync();
|
|
s.Clear();
|
|
}
|
|
}
|
|
|
|
await s.FlushAsync();
|
|
return "Success";
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.AsError($"Batch insert failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resource Management and Pooling Patterns
|
|
|
|
### **1. Object Pooling Pattern**
|
|
|
|
```csharp
|
|
public class ObjectPool<T> : IDisposable where T : class, IDisposable, new()
|
|
{
|
|
private readonly ConcurrentQueue<T> _objects = new();
|
|
private readonly Func<T> _objectFactory;
|
|
private readonly Action<T> _resetAction;
|
|
private readonly int _maxSize;
|
|
private int _currentCount = 0;
|
|
|
|
public ObjectPool(Func<T> objectFactory = null, Action<T> resetAction = null, int maxSize = 100)
|
|
{
|
|
_objectFactory = objectFactory ?? (() => new T());
|
|
_resetAction = resetAction;
|
|
_maxSize = maxSize;
|
|
}
|
|
|
|
public PooledObject<T> Get()
|
|
{
|
|
if (_objects.TryDequeue(out T item))
|
|
{
|
|
Interlocked.Decrement(ref _currentCount);
|
|
return new PooledObject<T>(item, this);
|
|
}
|
|
|
|
return new PooledObject<T>(_objectFactory(), this);
|
|
}
|
|
|
|
internal void Return(T item)
|
|
{
|
|
if (_currentCount < _maxSize)
|
|
{
|
|
_resetAction?.Invoke(item);
|
|
_objects.Enqueue(item);
|
|
Interlocked.Increment(ref _currentCount);
|
|
}
|
|
else
|
|
{
|
|
item.Dispose();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
while (_objects.TryDequeue(out T item))
|
|
{
|
|
item.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct PooledObject<T> : IDisposable where T : class, IDisposable
|
|
{
|
|
private readonly ObjectPool<T> _pool;
|
|
public T Object { get; }
|
|
|
|
internal PooledObject(T obj, ObjectPool<T> pool)
|
|
{
|
|
Object = obj;
|
|
_pool = pool;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_pool.Return(Object);
|
|
}
|
|
}
|
|
|
|
// Usage example - HTTP client pooling
|
|
public class HttpClientPool
|
|
{
|
|
private static readonly ObjectPool<HttpClient> _httpClientPool =
|
|
new ObjectPool<HttpClient>(
|
|
() => new HttpClient(),
|
|
client => { /* Reset client state */ },
|
|
50);
|
|
|
|
public static async Task<Result<string>> MakeRequestAsync(string url)
|
|
{
|
|
using var pooledClient = _httpClientPool.Get();
|
|
var client = pooledClient.Object;
|
|
|
|
try
|
|
{
|
|
var response = await client.GetStringAsync(url);
|
|
return Result<string>.AsSuccess(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<string>.AsError($"Request failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Memory Management Patterns**
|
|
|
|
```csharp
|
|
public class MemoryOptimizedService
|
|
{
|
|
private readonly MemoryPool<byte> _memoryPool = MemoryPool<byte>.Shared;
|
|
|
|
public async Task<Result<ProcessedData>> ProcessLargeDataAsync(Stream dataStream)
|
|
{
|
|
using var owner = _memoryPool.Rent(8192); // Rent 8KB buffer
|
|
var buffer = owner.Memory;
|
|
|
|
try
|
|
{
|
|
var processedData = new ProcessedData();
|
|
int bytesRead;
|
|
|
|
while ((bytesRead = await dataStream.ReadAsync(buffer)) > 0)
|
|
{
|
|
// Process data in chunks to avoid large allocations
|
|
var chunk = buffer.Slice(0, bytesRead);
|
|
ProcessChunk(chunk.Span, processedData);
|
|
}
|
|
|
|
return Result<ProcessedData>.AsSuccess(processedData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<ProcessedData>.AsError($"Processing failed: {ex.Message}");
|
|
}
|
|
// Buffer is automatically returned to pool when owner is disposed
|
|
}
|
|
|
|
private void ProcessChunk(Span<byte> chunk, ProcessedData result)
|
|
{
|
|
// Process data without additional allocations
|
|
for (int i = 0; i < chunk.Length; i++)
|
|
{
|
|
result.Checksum += chunk[i];
|
|
}
|
|
result.BytesProcessed += chunk.Length;
|
|
}
|
|
}
|
|
|
|
// Large object heap optimization
|
|
public class LargeDataProcessor
|
|
{
|
|
public Result<ProcessedResult> ProcessLargeDataset(IEnumerable<LargeDataItem> items)
|
|
{
|
|
try
|
|
{
|
|
var result = new ProcessedResult();
|
|
|
|
// Process in batches to avoid LOH pressure
|
|
const int batchSize = 1000;
|
|
var batch = new List<LargeDataItem>(batchSize);
|
|
|
|
foreach (var item in items)
|
|
{
|
|
batch.Add(item);
|
|
|
|
if (batch.Count == batchSize)
|
|
{
|
|
ProcessBatch(batch, result);
|
|
batch.Clear(); // Clear instead of creating new list
|
|
}
|
|
}
|
|
|
|
// Process remaining items
|
|
if (batch.Count > 0)
|
|
{
|
|
ProcessBatch(batch, result);
|
|
}
|
|
|
|
return Result<ProcessedResult>.AsSuccess(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<ProcessedResult>.AsError($"Large data processing failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void ProcessBatch(List<LargeDataItem> batch, ProcessedResult result)
|
|
{
|
|
// Process batch and update result
|
|
foreach (var item in batch)
|
|
{
|
|
result.ProcessedCount++;
|
|
result.TotalValue += item.Value;
|
|
}
|
|
|
|
// Force garbage collection after processing batch
|
|
// Only do this for large batches to avoid performance impact
|
|
if (batch.Count >= 1000)
|
|
{
|
|
GC.Collect(0, GCCollectionMode.Optimized);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Monitoring and Profiling Patterns
|
|
|
|
### **1. Performance Metrics Collection**
|
|
|
|
```csharp
|
|
public class PerformanceMonitoringService
|
|
{
|
|
private readonly IMetricsCollector _metricsCollector;
|
|
private readonly ILogger<PerformanceMonitoringService> _logger;
|
|
|
|
public async Task<Result<T>> MonitorOperationAsync<T>(
|
|
string operationName,
|
|
Func<Task<Result<T>>> operation,
|
|
Dictionary<string, object> additionalMetrics = null)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var startTime = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
var result = await operation();
|
|
|
|
stopwatch.Stop();
|
|
var duration = stopwatch.Elapsed;
|
|
|
|
// Collect performance metrics
|
|
var metrics = new OperationMetrics
|
|
{
|
|
OperationName = operationName,
|
|
Duration = duration,
|
|
Success = result.Status == ResultStatus.Success,
|
|
StartTime = startTime,
|
|
EndTime = DateTime.UtcNow,
|
|
AdditionalMetrics = additionalMetrics ?? new Dictionary<string, object>()
|
|
};
|
|
|
|
await _metricsCollector.RecordOperationAsync(metrics);
|
|
|
|
// Log slow operations
|
|
if (duration > TimeSpan.FromSeconds(5))
|
|
{
|
|
_logger.LogWarning("Slow operation detected: {Operation} took {Duration}ms",
|
|
operationName, duration.TotalMilliseconds);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
stopwatch.Stop();
|
|
|
|
await _metricsCollector.RecordOperationAsync(new OperationMetrics
|
|
{
|
|
OperationName = operationName,
|
|
Duration = stopwatch.Elapsed,
|
|
Success = false,
|
|
StartTime = startTime,
|
|
EndTime = DateTime.UtcNow,
|
|
Error = ex.Message
|
|
});
|
|
|
|
return Result<T>.AsError($"Operation {operationName} failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<Result<PerformanceReport>> GeneratePerformanceReportAsync(TimeSpan period)
|
|
{
|
|
try
|
|
{
|
|
var endTime = DateTime.UtcNow;
|
|
var startTime = endTime - period;
|
|
|
|
var metrics = await _metricsCollector.GetMetricsAsync(startTime, endTime);
|
|
|
|
var report = new PerformanceReport
|
|
{
|
|
Period = period,
|
|
TotalOperations = metrics.Count,
|
|
SuccessfulOperations = metrics.Count(m => m.Success),
|
|
AverageDuration = TimeSpan.FromMilliseconds(metrics.Average(m => m.Duration.TotalMilliseconds)),
|
|
SlowestOperations = metrics.OrderByDescending(m => m.Duration).Take(10).ToList(),
|
|
MostFrequentOperations = metrics.GroupBy(m => m.OperationName)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(10)
|
|
.ToDictionary(g => g.Key, g => g.Count())
|
|
};
|
|
|
|
return Result<PerformanceReport>.AsSuccess(report);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result<PerformanceReport>.AsError($"Failed to generate performance report: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Performance measurement attribute
|
|
[AttributeUsage(AttributeTargets.Method)]
|
|
public class MeasurePerformanceAttribute : Attribute
|
|
{
|
|
public string OperationName { get; set; }
|
|
public bool LogSlowOperations { get; set; } = true;
|
|
public int SlowThresholdMs { get; set; } = 1000;
|
|
}
|
|
|
|
// Performance interceptor
|
|
public class PerformanceInterceptor
|
|
{
|
|
private readonly PerformanceMonitoringService _performanceService;
|
|
|
|
public async Task<object> InterceptAsync(MethodInvocationContext context)
|
|
{
|
|
var perfAttribute = context.Method.GetCustomAttribute<MeasurePerformanceAttribute>();
|
|
if (perfAttribute == null)
|
|
return await context.ProceedAsync();
|
|
|
|
var operationName = perfAttribute.OperationName ?? $"{context.TargetType.Name}.{context.Method.Name}";
|
|
|
|
return await _performanceService.MonitorOperationAsync(operationName, async () =>
|
|
{
|
|
var result = await context.ProceedAsync();
|
|
return Result<object>.AsSuccess(result);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Resource Usage Monitoring**
|
|
|
|
```csharp
|
|
public class ResourceMonitoringService : BackgroundService
|
|
{
|
|
private readonly ILogger<ResourceMonitoringService> _logger;
|
|
private readonly IMetricsCollector _metricsCollector;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await CollectResourceMetricsAsync();
|
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error collecting resource metrics");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task CollectResourceMetricsAsync()
|
|
{
|
|
using var process = Process.GetCurrentProcess();
|
|
|
|
var metrics = new ResourceMetrics
|
|
{
|
|
Timestamp = DateTime.UtcNow,
|
|
WorkingSet = process.WorkingSet64,
|
|
PrivateMemory = process.PrivateMemorySize64,
|
|
VirtualMemory = process.VirtualMemorySize64,
|
|
ProcessorTime = process.TotalProcessorTime,
|
|
ThreadCount = process.Threads.Count,
|
|
HandleCount = process.HandleCount
|
|
};
|
|
|
|
// Add GC metrics
|
|
for (int generation = 0; generation <= GC.MaxGeneration; generation++)
|
|
{
|
|
metrics.GcCollections[generation] = GC.CollectionCount(generation);
|
|
}
|
|
|
|
metrics.TotalMemory = GC.GetTotalMemory(false);
|
|
|
|
await _metricsCollector.RecordResourceMetricsAsync(metrics);
|
|
|
|
// Alert on high resource usage
|
|
if (metrics.WorkingSet > 1_000_000_000) // > 1GB
|
|
{
|
|
_logger.LogWarning("High memory usage detected: {WorkingSet}MB",
|
|
metrics.WorkingSet / 1_000_000);
|
|
}
|
|
|
|
if (metrics.ThreadCount > 100)
|
|
{
|
|
_logger.LogWarning("High thread count detected: {ThreadCount}",
|
|
metrics.ThreadCount);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Best Practices and Guidelines
|
|
|
|
### **1. General Performance Guidelines**
|
|
- **Measure First**: Always measure performance before optimizing
|
|
- **Profile Regularly**: Use profiling tools to identify bottlenecks
|
|
- **Cache Strategically**: Cache expensive operations but avoid over-caching
|
|
- **Optimize Database Access**: Use proper indexing and query optimization
|
|
- **Manage Resources**: Properly dispose of resources and use pooling
|
|
|
|
### **2. Async Programming Guidelines**
|
|
- **Use ConfigureAwait(false)**: In library code to avoid deadlocks
|
|
- **Avoid Blocking**: Never use .Result or .Wait() on async methods
|
|
- **Parallel Processing**: Use Task.WhenAll for independent operations
|
|
- **Limit Concurrency**: Use SemaphoreSlim to control concurrent operations
|
|
|
|
### **3. Memory Management Guidelines**
|
|
- **Avoid Large Object Heap**: Keep objects under 85KB when possible
|
|
- **Use Object Pooling**: For frequently created/disposed objects
|
|
- **Stream Processing**: Process large datasets in chunks
|
|
- **Monitor GC Pressure**: Track garbage collection frequency and duration
|
|
|
|
### **4. Caching Guidelines**
|
|
- **Cache Expiry**: Set appropriate expiry times based on data volatility
|
|
- **Cache Invalidation**: Implement proper cache invalidation strategies
|
|
- **Cache Levels**: Use multi-level caching for optimal performance
|
|
- **Cache Monitoring**: Monitor cache hit rates and effectiveness
|
|
|
|
## Conclusion
|
|
|
|
The Centron application implements comprehensive performance patterns that ensure:
|
|
|
|
- **Scalability**: Multi-level caching and efficient resource management
|
|
- **Responsiveness**: Asynchronous programming and background processing
|
|
- **Efficiency**: Query optimization and memory management
|
|
- **Monitoring**: Performance metrics collection and resource monitoring
|
|
- **Maintainability**: Clear patterns and best practices for performance optimization
|
|
|
|
These performance patterns provide the foundation for a high-performance enterprise application that can scale with growing user demands while maintaining optimal response times and resource utilization. |