Service Architecture Pattern
Overview
This pattern defines the standard service architecture used across all MepDash dashboards. The architecture is built on dependency injection, interface segregation, and single responsibility principles to provide a maintainable, testable, and loosely-coupled service layer.
Core Concepts
- Dependency Injection (DI): IoC container manages service instantiation and lifetime
- Interface Segregation: Each service has a focused interface defining its contract
- Service Lifetimes: Singleton, Transient, and Scoped services with appropriate lifecycles
- Service Composition: Complex operations through service orchestration
- Cross-Cutting Concerns: Logging, validation, and error handling across all services
Pattern Implementation
Service Layer Structure
All dashboards follow a hierarchical service organization:
Services/
├── Core Services (Business Logic)
│ ├── {Feature}Service # Main business logic service
│ └── SysproPostService # SYSPRO integration service
├── Infrastructure Services
│ ├── DatabaseValidationService # Database validation logic
│ └── CustomFormService # Custom form handling
├── Utility Services
│ ├── MepSettingsService # Settings management
│ └── ViewFactoryService # View creation factory
└── Base Services
└── RegisterServiceProvider # Service registration
Service Registration Pattern
All dashboards implement a central service registration pattern using either ServiceCollection or dependency injection containers:
// Pattern: Service Registration
public static class RegisterServiceProvider
{
public static IServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
// Core Services - Singleton for shared state
services.AddSingleton<IBusinessService, BusinessService>();
services.AddSingleton<ISysproPostService, SysproPostService>();
// Infrastructure Services - Singleton for configuration
services.AddSingleton<IDatabaseValidationService, DatabaseValidationService>();
services.AddSingleton<IMepSettingsService, MepSettingsService>();
// Data Context - Transient/Scoped for isolation
services.AddScoped<PluginSysproDataContext>();
// ViewModels - Transient for new instances
services.AddTransient<IMainViewModel, MainViewModel>();
// External Dependencies
services.AddSingleton<ILogger>(provider =>
provider.GetService<ILoggerFactory>().CreateLogger("Dashboard"));
return services.BuildServiceProvider();
}
// Alternative pattern with IServiceCollection
public static IServiceCollection RegisterServices(
IMepPluginServiceHandler mepPluginServiceHandler,
ISharedShellInterface sharedShellInterface)
{
IServiceCollection serviceCollection = new ServiceCollection();
// Register MepDash plugin services
serviceCollection = mepPluginServiceHandler
.RegisterMepPluginServices(serviceCollection);
// Register application services
serviceCollection
.AddSingleton<MainViewModel>()
.AddSingleton<PluginSysproDataContext>()
.AddSingleton<IFeatureService, FeatureService>();
return serviceCollection;
}
}
Base Service Pattern with Abstract Classes
For dashboards requiring a base service infrastructure:
// Pattern: Abstract Service Registration
public abstract class ARegisterService
{
public abstract void RegisterServices(IServiceCollection services);
protected void RegisterCommonServices(IServiceCollection services)
{
// Register logging
services.AddLogging();
// Register configuration
services.AddSingleton<IConfiguration>(Configuration);
}
}
// Concrete implementation
public class RegisterServiceProvider : ARegisterService
{
public override void RegisterServices(IServiceCollection services)
{
// Data Context Registration
services.AddScoped<PluginSysproDataContext>();
// Business Services
services.AddTransient<BusinessService>();
// Register Views and ViewModels
RegisterViews(services);
}
}
Service Lifetime Management
Singleton Services
Long-lived services that maintain state across the application lifetime:
// Pattern: Singleton Service with Thread-Safe State
public class MepSettingsService : IMepSettingsService
{
private readonly ILogger<MepSettingsService> _logger;
private MepSettings _settings;
private readonly object _lockObject = new object();
public MepSettingsService(ILogger<MepSettingsService> logger)
{
_logger = logger;
LoadSettings();
}
private void LoadSettings()
{
lock (_lockObject)
{
var settingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"Resources", "MepSettings.json");
if (File.Exists(settingsPath))
{
var json = File.ReadAllText(settingsPath);
_settings = JsonSerializer.Deserialize<MepSettings>(json);
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Settings loaded successfully. {@MepSettingsContext}",
new { settingsPath, settings = _settings });
}
}
}
}
public MepSettings GetSettings()
{
lock (_lockObject)
{
return _settings?.Clone() ?? new MepSettings();
}
}
}
Transient Services
Short-lived services created for each request:
// Pattern: Transient Service
public class BusinessService
{
private readonly ILogger<BusinessService> _logger;
private readonly PluginSysproDataContext _dataContext;
private readonly ISysproPostService _sysproPost;
public BusinessService(
ILogger<BusinessService> logger,
PluginSysproDataContext dataContext,
ISysproPostService sysproPost)
{
_logger = logger;
_dataContext = dataContext;
_sysproPost = sysproPost;
}
// Service methods...
}
Scoped Services
Services that live for the duration of a request or scope:
// Pattern: Scoped Database Context
public class PluginSysproDataContext : DbContext
{
public PluginSysproDataContext() : base("name=SysproEntities")
{
// Each scope gets its own context instance
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Proper cleanup for scoped service
base.Dispose();
}
}
}
Dependency Injection Pattern
All services follow constructor injection for dependencies:
// Pattern: Constructor Dependency Injection
public class FeatureService : IFeatureService
{
private readonly ILogger<FeatureService> _logger;
private readonly IDataContext _context;
private readonly ISharedShellInterface _sharedShellInterface;
public FeatureService(
ILogger<FeatureService> logger,
IDataContext context,
ISharedShellInterface sharedShellInterface)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_context = context ?? throw new ArgumentNullException(nameof(context));
_sharedShellInterface = sharedShellInterface
?? throw new ArgumentNullException(nameof(sharedShellInterface));
}
}
Service Interface Definition
Services expose their functionality through well-defined interfaces:
// Pattern: Service Interface
public interface IBusinessService
{
// Query operations
Task<IEnumerable<T>> QueryDataAsync<T>(Expression<Func<T, bool>> predicate);
Task<T> GetByIdAsync<T>(int id);
// Command operations
Task<OperationResult> CreateAsync<T>(T entity);
Task<OperationResult> UpdateAsync<T>(T entity);
Task<OperationResult> DeleteAsync<T>(int id);
// Business operations
Task<ProcessResult> ProcessBusinessLogicAsync(BusinessRequest request);
Task<ValidationResult> ValidateBusinessRulesAsync(BusinessData data);
}
Service Composition Pattern
Complex operations are achieved through service orchestration:
// Pattern: Service Composition
public class WorkflowService
{
private readonly IBusinessService _businessService;
private readonly IValidationService _validationService;
private readonly INotificationService _notificationService;
private readonly ILogger<WorkflowService> _logger;
public async Task<WorkflowResult> ExecuteWorkflowAsync(WorkflowRequest request)
{
try
{
// Step 1: Validate
_logger.LogInformation("Validating workflow request {@WorkflowContext}",
new { request });
var validationResult = await _validationService.ValidateAsync(request);
if (!validationResult.IsValid)
{
_logger.LogWarning("Validation failed {@ValidationContext}",
new { request, validationResult });
return WorkflowResult.ValidationFailed(validationResult);
}
// Step 2: Process business logic
_logger.LogInformation("Processing business logic {@BusinessContext}",
new { request });
var processResult = await _businessService.ProcessAsync(request);
// Step 3: Send notifications
if (processResult.Success)
{
await _notificationService.NotifyAsync(processResult);
}
return WorkflowResult.Success(processResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Workflow execution failed {@ErrorContext}",
new { request });
throw;
}
}
}
Dashboard-Specific Implementations
AR Payment Reversal
- Uses
ArReversePaymentServicefor payment reversal operations - Implements
IQueueManagementServicefor batch processing - Singleton services for state management across queue operations
- See: AR Service Implementation
Inventory Mini MRP
- Uses
InvOrderingServicefor MRP calculations and order creation - Implements transient services for stateless operations
- Scoped database context for request isolation
- See: Inventory Service Implementation
AP EFT Remittance
- Uses
EftRemittanceServicefor remittance processing - All singleton services for state preservation
- Integrates with SSRS reporting services
- See: AP Service Implementation
Best Practices
-
Use appropriate service lifetimes:
- Singleton for shared state and configuration
- Scoped for database contexts
- Transient for stateless operations
-
Follow interface segregation:
- Keep interfaces focused and cohesive
- Avoid fat interfaces with too many responsibilities
-
Implement proper disposal:
- Implement IDisposable for services managing resources
- Use proper cleanup in Dispose methods
-
Use constructor injection:
- Inject all dependencies through constructors
- Validate dependencies with null checks
-
Handle cross-cutting concerns:
- Use structured logging with appropriate log levels
- Implement proper error handling and recovery