Service Architecture
Overview
The service layer architecture in MepApps.Dash.Inv.Batch.MiniMrpOrderCreation implements a comprehensive dependency injection pattern that provides business logic, data access, and cross-cutting concerns through a well-organized service infrastructure. The architecture follows SOLID principles and ensures loose coupling, testability, and maintainability across the application.
Key Concepts
- Dependency Injection: Services are injected through constructors using Microsoft.Extensions.DependencyInjection
- Service Lifetimes: Transient, Scoped, and Singleton patterns for different service types
- Interface Segregation: Small, focused interfaces for specific responsibilities
- Service Composition: Complex operations through service orchestration
Implementation Details
Core Service Infrastructure
The service architecture is built on the ARegisterService base class pattern:
// Services/ARegisterService.cs
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);
}
}
Service Registration Pattern
The RegisterServiceProvider class orchestrates all service registrations:
// Services/RegisterServiceProvider.cs
public class RegisterServiceProvider : ARegisterService
{
public override void RegisterServices(IServiceCollection services)
{
// Data Context Registration
services.AddScoped<PluginSysproDataContext>();
// Business Services
services.AddTransient<InvOrderingService>();
services.AddTransient<SysproPostService>();
// Utility Services
services.AddSingleton<IMepSettingsService, MepSettingsService>();
services.AddTransient<IDatabaseValidationService, DatabaseValidationService>();
// Navigation Services
services.AddTransient<NavToOrderCompletion>();
// View Models
services.AddTransient<MainViewModel>();
services.AddTransient<WarehouseOrderingViewModel>();
services.AddTransient<InvOrderingCompletionViewModel>();
// Dialog View Models
services.AddTransient<SupplierSelectionViewModel>();
services.AddTransient<SalesOrderAllocationsViewModel>();
services.AddTransient<WipAllocationsViewModel>();
// Views
RegisterViews(services);
}
private void RegisterViews(IServiceCollection services)
{
services.AddTransient<MainView>();
services.AddTransient<WarehouseOrderingView>();
services.AddTransient<InvOrderingCompletionView>();
// Dialog Views
services.AddTransient<SupplierSelectionView>();
services.AddTransient<SalesOrderAllocationsView>();
services.AddTransient<WipAllocationsView>();
}
}
Service Lifetime Management
1. Transient Services
Created each time they are requested:
// InvOrderingService.cs - Transient service example
public class InvOrderingService
{
private readonly ILogger<InvOrderingService> _logger;
private readonly PluginSysproDataContext _sysproDataContext;
private readonly SysproPostService _sysproPost;
public InvOrderingService(
ILogger<InvOrderingService> logger,
PluginSysproDataContext sysproDataContext,
SysproPostService sysproPost)
{
_logger = logger;
_sysproDataContext = sysproDataContext;
_sysproPost = sysproPost;
}
// Service methods...
}
2. Scoped Services
Created once per request/scope:
// Db/PluginSysproDataContext.cs - Scoped service
public class PluginSysproDataContext : DbContext
{
// Scoped to ensure same context throughout request
public DbSet<ApSupplier> ApSupplier { get; set; }
public DbSet<InvMaster> InvMaster { get; set; }
public DbSet<InvWarehouse> InvWarehouse { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Configuration...
}
}
3. Singleton Services
Single instance for application lifetime:
// MepSettingsService.cs (lines 19-29) - Singleton service
public class MepSettingsService : IMepSettingsService
{
private readonly ILogger<IMepSettingsService> _logger;
private readonly ISysproTenantPreferenceManager _sysproTenantPreferenceManager;
// Singleton state - shared across all requests
public List<MepSettings> Settings { get; private set; } = new List<MepSettings>();
public MepSettingsService(
ILogger<IMepSettingsService> logger,
ISysproTenantPreferenceManager sysproTenantPreferenceManager)
{
_logger = logger;
_sysproTenantPreferenceManager = sysproTenantPreferenceManager;
}
}
Service Interface Definitions
Services are defined through interfaces for abstraction:
// Services/IDatabaseValidationService.cs (lines 7-16)
public interface IDatabaseValidationService
{
Task InitializeAsync();
Task CreateMissingDatabaseViewsAsync();
Task<bool> ViewExistsSysproAsync(string viewname);
Task ValidateAndUpdateExistingViewsAsync();
Task<bool> ViewColumnExistsSysproAsync(string databaseValidationObjectName);
Task RunScriptResourceSysproAsync(string scriptResource);
List<DatabaseValidationObject> DatabaseValidationObjects { get; }
}
// Services/IMepSettingsService.cs (lines 6-17)
public interface IMepSettingsService
{
List<MepSettings> Settings { get; }
Task InitializeDefaultAsync();
string Get(string settingName);
bool GetBool(string settingName, bool defaultValue = false);
Task SetByProgramAsync(string settingName, string settingValue);
Task SetByCompanyAsync(string settingName, string settingValue);
Task SetByCompanyOperatorAsync(string settingName, string settingValue);
Task SetByOperatorAsync(string settingName, string settingValue);
List<string> GetList(string settingName);
}
Service Composition and Orchestration
Complex operations are achieved through service composition:
// InvOrderingService.cs (lines 241-272) - Service orchestration
public async Task<CreateOrdersResult> CreateOrders(
IEnumerable<SummaryOrder> orderSummaries,
string selectedBuyer)
{
var postResult = new CreateOrdersResult(selectedBuyer);
try
{
// Orchestrate multiple services
postResult.Orders = _sysproPost.CreateOrders(orderSummaries).ToList();
foreach (var order in postResult.Orders)
{
order.PostFailed = order.OrderNumber == "FAILED" ||
string.IsNullOrWhiteSpace(order.OrderNumber);
if (order.PostFailed)
{
_logger.LogInformation("Order creation failed for {Supplier} {Warehouse} {DueDate}",
order.Supplier, order.Warehouse, order.DueDate);
continue;
}
_logger.LogInformation("Purchase order {OrderNumber} created", order.OrderNumber);
// Compose multiple data operations
order.PurchaseOrderHeader = await QueryPurchaseOrderHeaderAsync(order.OrderNumber)
.ConfigureAwait(true);
List<PurchaseOrderLineLookup> poLines = await GetPurchaseOrderLinesForPuchaseOrder(
order.OrderNumber).ConfigureAwait(true);
order.LineCount = poLines?.Count ?? 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating orders {@Orders}", orderSummaries);
throw;
}
return postResult;
}
Service Initialization Patterns
Services implement various initialization strategies:
// DatabaseValidationService.cs (lines 31-55) - Async initialization
public async Task InitializeAsync()
{
try
{
string validationJson = ResourceHelper.GetResourceFileText("DatabaseValidation.json");
if (string.IsNullOrEmpty(validationJson))
{
_logger.LogWarning("ValidationJson is NULL or EMPTY. RunValidationAsync aborted.");
return;
}
_logger.LogInformation("Deserializing DatabaseValidation.json content. Content={0}",
validationJson);
DatabaseValidationObjects = JsonConvert.DeserializeObject<List<DatabaseValidationObject>>(
validationJson);
if (DatabaseValidationObjects == null)
{
_logger.LogError("DatabaseValidationObjects object is NULL after deserializing");
throw new Exception("DatabaseValidationObjects object is NULL");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in InitializeAsync");
throw;
}
}
Cross-Cutting Concerns
Services handle cross-cutting concerns through aspect-oriented patterns:
1. Logging
// Structured logging throughout services
_logger.LogInformation("Order creation completed");
_logger.LogError(ex, "Error creating orders {@Orders}", orders);
_logger.LogTrace("Query returned result. {@QueryContext}", new { context, result });
2. Error Handling
// Consistent error handling pattern
public async Task<T> ExecuteWithErrorHandling<T>(Func<Task<T>> operation)
{
try
{
return await operation();
}
catch (DbException dbEx)
{
_logger.LogError(dbEx, "Database operation failed");
throw new DataAccessException("Unable to access database", dbEx);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in service operation");
throw;
}
}
3. Transaction Management
// Transaction scope for data consistency
public async Task<bool> ProcessOrdersInTransaction(IEnumerable<Order> orders)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
foreach (var order in orders)
{
await ProcessOrder(order);
}
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
Examples
Example 1: Creating a New Service
// Define interface
public interface IInventoryCalculationService
{
Task<decimal> CalculateReorderQuantityAsync(string stockCode, string warehouse);
Task<bool> ValidateStockLevelsAsync(string stockCode);
}
// Implement service
public class InventoryCalculationService : IInventoryCalculationService
{
private readonly ILogger<InventoryCalculationService> _logger;
private readonly PluginSysproDataContext _context;
public InventoryCalculationService(
ILogger<InventoryCalculationService> logger,
PluginSysproDataContext context)
{
_logger = logger;
_context = context;
}
public async Task<decimal> CalculateReorderQuantityAsync(
string stockCode, string warehouse)
{
// Implementation
}
}
// Register service
services.AddTransient<IInventoryCalculationService, InventoryCalculationService>();
Example 2: Service Dependency Chain
// WarehouseOrderingViewModel depends on multiple services
public WarehouseOrderingViewModel(
ILogger<WarehouseOrderingViewModel> logger,
IExcelExportService excelExportService,
InvOrderingService invOrderingService,
NavToOrderCompletion navToOrderCompletion,
IMepSettingsService mepSettingsService)
{
// All dependencies injected automatically
_logger = logger;
_excelExportService = excelExportService;
_invOrderingService = invOrderingService;
_navToOrderCompletion = navToOrderCompletion;
_mepSettingsService = mepSettingsService;
}
Example 3: Service Factory Pattern
// ViewFactoryService.cs - Factory pattern for view creation
public class ViewFactoryService : IViewFactoryService
{
private readonly IServiceProvider _serviceProvider;
public ViewFactoryService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T CreateView<T>() where T : UserControl
{
// Use service provider to create view with dependencies
return _serviceProvider.GetService<T>();
}
public object CreateViewByType(Type viewType)
{
return _serviceProvider.GetService(viewType);
}
}
Best Practices
Service Design Principles
- Keep services focused on single responsibility
- Use interfaces for all public services
- Avoid circular dependencies
- Prefer composition over inheritance
Dependency Injection Guidelines
- Use constructor injection for required dependencies
- Avoid service locator pattern except in factories
- Register services with appropriate lifetimes
- Use factory delegates for complex creation logic
Error Handling Strategies
- Let exceptions bubble up to appropriate handlers
- Log errors with context at service boundaries
- Provide meaningful error messages
- Use custom exceptions for domain-specific errors
Testing Considerations
- Design services to be easily mockable
- Avoid static dependencies
- Use interfaces for external dependencies
- Keep business logic separate from infrastructure
Common Pitfalls
Incorrect Service Lifetime
// BAD: Singleton service with scoped dependency
services.AddSingleton<MySingletonService>(); // Has DbContext dependency
services.AddScoped<DbContext>();
// GOOD: Matching lifetimes
services.AddScoped<MyService>();
services.AddScoped<DbContext>();
Service Locator Anti-Pattern
// BAD: Using service locator
public class MyService
{
public void DoWork()
{
var dep = ServiceLocator.Get<IDependency>(); // Anti-pattern
}
}
// GOOD: Constructor injection
public class MyService
{
private readonly IDependency _dependency;
public MyService(IDependency dependency)
{
_dependency = dependency;
}
}
Circular Dependencies
// BAD: Services depend on each other
public ServiceA(ServiceB b) { }
public ServiceB(ServiceA a) { } // Circular!
// GOOD: Break circular dependency with third service or events
public ServiceA(IEventBus bus) { }
public ServiceB(IEventBus bus) { }
Related Documentation
Summary
The service architecture in MiniMrpOrderCreation provides a robust, scalable foundation for business logic implementation. Through proper use of dependency injection, service lifetimes, and composition patterns, the architecture maintains loose coupling while ensuring services are easily testable and maintainable. The consistent approach to error handling, logging, and initialization ensures reliable operation across all service layers.