Business Logic Services Pattern
Overview
This pattern defines how business logic services encapsulate core domain logic across MepDash dashboards. These services orchestrate complex workflows, implement business rules, and manage domain-specific operations while maintaining clean separation from infrastructure and presentation concerns.
Core Concepts
- Domain-Driven Design: Business logic aligned with domain requirements
- Workflow Orchestration: Multi-step process coordination
- Business Rule Validation: Enforcement of business constraints
- State Management: Tracking entity states through business processes
- Error Recovery: Resilient processing with compensation logic
Pattern Implementation
Core Business Service Structure
All dashboards implement a primary business service that orchestrates domain operations:
// Pattern: Core Business Service
public interface IBusinessService
{
// Query operations
Task<IEnumerable<DomainEntity>> GetBusinessEntitiesAsync(BusinessCriteria criteria);
Task<DomainEntity> GetBusinessEntityAsync(string id);
// Command operations
Task<ProcessResult> ProcessBusinessOperationAsync(BusinessRequest request);
Task<ValidationResult> ValidateBusinessRulesAsync(BusinessData data);
// Workflow operations
Task<WorkflowResult> ExecuteWorkflowAsync(WorkflowRequest request);
Task<BatchResult> ProcessBatchAsync(IEnumerable<BatchItem> items);
}
public class BusinessService : IBusinessService
{
private readonly ILogger<BusinessService> _logger;
private readonly IDataService _dataService;
private readonly ISysproPostService _sysproService;
private readonly IValidationService _validationService;
public BusinessService(
ILogger<BusinessService> logger,
IDataService dataService,
ISysproPostService sysproService,
IValidationService validationService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
_sysproService = sysproService ?? throw new ArgumentNullException(nameof(sysproService));
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
}
}
Workflow Orchestration Pattern
Complex business processes are implemented as orchestrated workflows:
// Pattern: Workflow Orchestration
public class WorkflowOrchestrator
{
private readonly ILogger<WorkflowOrchestrator> _logger;
public async Task<WorkflowResult> ExecuteWorkflowAsync(WorkflowRequest request)
{
var workflow = new WorkflowResult
{
WorkflowId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow
};
try
{
// Step 1: Validation Phase
_logger.LogInformation("Starting workflow validation {@WorkflowContext}",
new { request.WorkflowType, request.RequestId });
workflow.ValidationResult = await ValidateWorkflowRequest(request);
if (!workflow.ValidationResult.IsValid)
{
workflow.Status = WorkflowStatus.ValidationFailed;
return workflow;
}
// Step 2: Preparation Phase
_logger.LogInformation("Preparing workflow data {@WorkflowContext}",
new { request.RequestId });
workflow.PreparedData = await PrepareWorkflowData(request);
// Step 3: Execution Phase
_logger.LogInformation("Executing workflow operations {@WorkflowContext}",
new { request.RequestId });
workflow.ExecutionResult = await ExecuteWorkflowOperations(
workflow.PreparedData);
// Step 4: Post-Processing Phase
if (workflow.ExecutionResult.Success)
{
_logger.LogInformation("Post-processing workflow {@WorkflowContext}",
new { request.RequestId });
workflow.PostProcessResult = await PostProcessWorkflow(
workflow.ExecutionResult);
}
// Step 5: Completion
workflow.Status = WorkflowStatus.Completed;
workflow.EndTime = DateTime.UtcNow;
_logger.LogInformation("Workflow completed successfully {@WorkflowResult}",
workflow);
}
catch (Exception ex)
{
_logger.LogError(ex, "Workflow failed {@ErrorContext}",
new { request.RequestId, workflow.CurrentStep });
// Execute compensation logic
await CompensateWorkflow(workflow);
workflow.Status = WorkflowStatus.Failed;
workflow.Error = ex.Message;
}
return workflow;
}
private async Task CompensateWorkflow(WorkflowResult workflow)
{
// Implement compensation logic to rollback partial changes
if (workflow.ExecutionResult?.PartialResults != null)
{
foreach (var partial in workflow.ExecutionResult.PartialResults.Reverse())
{
await RollbackOperation(partial);
}
}
}
}
Business Rule Validation Pattern
Business rules are validated consistently across all operations:
// Pattern: Business Rule Validation
public class BusinessRuleValidator
{
private readonly ILogger<BusinessRuleValidator> _logger;
private readonly IDataService _dataService;
public async Task<ValidationResult> ValidateBusinessRulesAsync<T>(
T entity,
ValidationContext context) where T : class
{
var result = new ValidationResult();
// Apply validation rules based on type
var validators = GetValidators<T>(context);
foreach (var validator in validators)
{
var validationOutcome = await validator.ValidateAsync(entity);
if (!validationOutcome.IsValid)
{
result.Errors.AddRange(validationOutcome.Errors);
_logger.LogWarning("Validation failed {@ValidationContext}",
new {
EntityType = typeof(T).Name,
Errors = validationOutcome.Errors,
Context = context
});
}
}
result.IsValid = !result.Errors.Any();
return result;
}
// Example specific validation
public async Task<ValidationResult> ValidatePaymentReversalAsync(
PaymentReversalRequest request)
{
var result = new ValidationResult();
// Business Rule 1: Payment must exist
var payment = await _dataService.GetPaymentAsync(request.PaymentNumber);
if (payment == null)
{
result.AddError("Payment", $"Payment {request.PaymentNumber} not found");
}
// Business Rule 2: Payment must not be already reversed
if (payment?.IsReversed == true)
{
result.AddError("Payment", "Payment has already been reversed");
}
// Business Rule 3: Reversal date must be in valid period
if (!await IsValidReversalPeriod(request.ReversalDate))
{
result.AddError("Period", "Reversal date is not in an open period");
}
// Business Rule 4: User must have permission
if (!await UserHasPermission(request.UserId, "ReversePayment"))
{
result.AddError("Permission", "User lacks permission to reverse payments");
}
return result;
}
}
Calculation Engine Pattern
Complex business calculations are encapsulated in dedicated services:
// Pattern: Business Calculation Engine
public class CalculationEngine
{
private readonly ILogger<CalculationEngine> _logger;
private readonly IDataService _dataService;
// MRP Calculation Example
public async Task<MrpCalculationResult> CalculateMrpRequirementsAsync(
MrpParameters parameters)
{
var result = new MrpCalculationResult();
try
{
// Get current inventory levels
var inventory = await _dataService.GetInventoryLevelsAsync(
parameters.StockCodes);
// Get demand forecast
var demand = await CalculateDemandForecastAsync(parameters);
// Calculate requirements for each item
foreach (var item in inventory)
{
var requirement = new MrpRequirement
{
StockCode = item.StockCode,
CurrentStock = item.QtyOnHand,
SafetyStock = item.SafetyStockQty,
MinimumOrderQty = item.MinOrderQty,
LeadTimeDays = item.LeadTime
};
// Apply MRP formula
var projectedDemand = demand[item.StockCode];
var availableStock = item.QtyOnHand - item.QtyAllocated;
var reorderPoint = item.SafetyStockQty +
(projectedDemand * item.LeadTime / 30);
if (availableStock < reorderPoint)
{
requirement.RequiredQty = Math.Max(
reorderPoint - availableStock,
item.MinOrderQty
);
// Round to order multiple if specified
if (item.OrderMultiple > 0)
{
requirement.RequiredQty = Math.Ceiling(
requirement.RequiredQty / item.OrderMultiple
) * item.OrderMultiple;
}
requirement.SuggestedOrderDate = DateTime.Now;
requirement.RequiredDate = DateTime.Now.AddDays(item.LeadTime);
result.Requirements.Add(requirement);
}
}
_logger.LogInformation("MRP calculation completed {@MrpContext}",
new {
ItemCount = inventory.Count,
RequirementsCount = result.Requirements.Count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "MRP calculation failed {@ErrorContext}",
parameters);
throw;
}
return result;
}
}
Batch Processing Pattern
Efficient processing of multiple items with transaction support:
// Pattern: Batch Processing
public class BatchProcessor
{
private readonly ILogger<BatchProcessor> _logger;
private readonly IDataService _dataService;
private readonly IValidationService _validationService;
public async Task<BatchResult> ProcessBatchAsync<T>(
IEnumerable<T> items,
Func<T, Task<ProcessResult>> processor,
BatchOptions options = null) where T : class
{
options ??= new BatchOptions();
var batchResult = new BatchResult
{
BatchId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow,
TotalItems = items.Count()
};
var validItems = new List<T>();
// Validation phase
foreach (var item in items)
{
var validation = await _validationService.ValidateAsync(item);
if (validation.IsValid)
{
validItems.Add(item);
}
else
{
batchResult.FailedItems.Add(new FailedItem
{
Item = item,
Errors = validation.Errors
});
}
}
// Processing phase
if (options.UseTransaction)
{
await ProcessWithTransactionAsync(validItems, processor, batchResult);
}
else
{
await ProcessWithoutTransactionAsync(validItems, processor, batchResult);
}
batchResult.EndTime = DateTime.UtcNow;
batchResult.SuccessCount = batchResult.ProcessedItems.Count;
batchResult.FailureCount = batchResult.FailedItems.Count;
_logger.LogInformation("Batch processing completed {@BatchResult}",
batchResult);
return batchResult;
}
private async Task ProcessWithTransactionAsync<T>(
List<T> items,
Func<T, Task<ProcessResult>> processor,
BatchResult batchResult) where T : class
{
using (var transaction = await _dataService.BeginTransactionAsync())
{
try
{
foreach (var item in items)
{
var result = await processor(item);
if (result.Success)
{
batchResult.ProcessedItems.Add(item);
}
else
{
// Fail entire batch on error
throw new BatchProcessingException(
$"Item processing failed: {result.ErrorMessage}");
}
}
await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Batch transaction failed");
// Move all items to failed
batchResult.FailedItems.AddRange(
items.Select(i => new FailedItem
{
Item = i,
Errors = new[] { ex.Message }
}));
batchResult.ProcessedItems.Clear();
}
}
}
}
State Management Pattern
Managing entity states through business processes:
// Pattern: State Management
public class StateManager<TEntity> where TEntity : IStateful
{
private readonly ILogger<StateManager<TEntity>> _logger;
private readonly IDataService _dataService;
public async Task<StateTransitionResult> TransitionStateAsync(
TEntity entity,
string targetState,
StateContext context)
{
var result = new StateTransitionResult
{
FromState = entity.CurrentState,
TargetState = targetState
};
try
{
// Validate transition
if (!IsValidTransition(entity.CurrentState, targetState))
{
result.Success = false;
result.Error = $"Invalid transition from {entity.CurrentState} to {targetState}";
return result;
}
// Execute pre-transition actions
await ExecutePreTransitionActions(entity, targetState, context);
// Update state
var previousState = entity.CurrentState;
entity.CurrentState = targetState;
entity.StateChangedDate = DateTime.UtcNow;
entity.StateChangedBy = context.UserId;
// Persist change
await _dataService.UpdateAsync(entity);
// Execute post-transition actions
await ExecutePostTransitionActions(entity, previousState, context);
result.Success = true;
result.NewState = targetState;
_logger.LogInformation("State transition completed {@StateContext}",
new {
EntityId = entity.Id,
FromState = previousState,
ToState = targetState
});
}
catch (Exception ex)
{
_logger.LogError(ex, "State transition failed {@ErrorContext}",
new { entity.Id, targetState });
result.Success = false;
result.Error = ex.Message;
}
return result;
}
}
Dashboard-Specific Implementations
AR Payment Reversal
ArReversePaymentServiceorchestrates payment reversal workflows- Queue management for staging reversals
- Multi-step validation and posting process
- See: AR Business Logic
Inventory Mini MRP
InvOrderingServiceimplements MRP calculations- Order consolidation by supplier and warehouse
- Safety stock and minimum quantity calculations
- See: Inventory Business Logic
AP EFT Remittance
EftRemittanceServicemanages remittance processing- Email distribution workflow
- Payment selection and validation
- See: AP Business Logic
Best Practices
-
Separate business logic from infrastructure
- Keep domain logic independent of data access
- Use interfaces for external dependencies
-
Implement comprehensive validation
- Validate early in the process
- Provide clear, actionable error messages
-
Use structured logging
- Log business events with context
- Distinguish business errors from technical errors
-
Design for failure recovery
- Implement compensation logic
- Support partial success scenarios
-
Optimize batch operations
- Process in chunks for large datasets
- Use transactions appropriately
-
Document business rules
- Make implicit rules explicit
- Version business logic changes
Common Pitfalls
- Mixing concerns - Business logic in UI or data layers
- Missing validation - Not checking business constraints
- No compensation - Unable to recover from failures
- Poor error messages - Technical errors exposed to users
- Synchronous processing - Blocking on long operations