Service Architecture
Overview
The AR Payment Reversal dashboard implements a comprehensive service layer architecture based on dependency injection, interface segregation, and single responsibility principles. This document explains the overall service architecture, dependency injection patterns, service lifetimes, and how services are composed to deliver business functionality.
Key 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
Implementation Details
Service Layer Structure
The service layer follows a hierarchical organization:
Services/
├── Core Services (Business Logic)
│ ├── ArReversePaymentService
│ └── SysproPostService
├── Infrastructure Services
│ ├── DatabaseValidationService
│ └── CustomFormService
├── Utility Services
│ ├── MepSettingsService
│ └── ViewFactoryService
└── Base Services
└── RegisterServiceProvider
Dependency Injection Configuration
Service Registration Pattern
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/RegisterServiceProvider.cs
public static class RegisterServiceProvider
{
public static IServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
// Core Services - Singleton for shared state
services.AddSingleton<IArReversePaymentService, ArReversePaymentService>();
services.AddSingleton<ISysproPostService, SysproPostService>();
// Infrastructure Services - Singleton for configuration
services.AddSingleton<IDatabaseValidationService, DatabaseValidationService>();
services.AddSingleton<ICustomFormService, CustomFormService>();
services.AddSingleton<IMepSettingsService, MepSettingsService>();
// Data Context - Transient for isolation
services.AddTransient<PluginSysproDataContext>();
// ViewModels - Transient for new instances
services.AddTransient<IMainViewModel, MainViewModel>();
services.AddTransient<IArReversePaymentQueueViewModel, ArReversePaymentQueueViewModel>();
// External Dependencies
services.AddSingleton<ILogger>(provider =>
provider.GetService<ILoggerFactory>().CreateLogger("ArPaymentReversal"));
services.AddSingleton<ISysproEnet>(provider =>
MainView.MepPluginServiceProvider.GetService<ISysproEnet>());
return services.BuildServiceProvider();
}
}
Service Lifetime Management
Singleton Services
Long-lived services that maintain state across the application:
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/MepSettingsService.cs
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:
// MepApps.Dash.Ar.Maint.PaymentReversal/Db/PluginSysproDataContext.cs
public class PluginSysproDataContext : DbContext, IDisposable
{
public PluginSysproDataContext() : base("name=SysproEntities")
{
// Each instance gets its own context
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
// DbSets for entity access
public DbSet<ArCustomer> ArCustomers { get; set; }
public DbSet<ArInvoice> ArInvoices { get; set; }
public DbSet<CG_ArReversePaymentQueueHeader> C_ArReversePaymentQueueHeader { get; set; }
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Proper cleanup for transient service
base.Dispose();
}
}
}
Service Interface Design
Interface Segregation Principle
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/ArReversePaymentService.cs (Lines 9-23)
public interface IArReversePaymentService
{
// Query Operations
Task<IEnumerable<ArReversePaymentHeader>> GetQueuedPaymentHeaders();
Task<CustomerItem> QueryCustomer(string customer);
Task<IEnumerable<ArReversePaymentHeader>> QueryCustomerPaymentsAsync(string customer);
Task<IEnumerable<ArReversePaymentDetail>> QueryPaymentInvoicesAsync(IEnumerable<ArReversePaymentHeader> payments);
Task<IEnumerable<SelectionItem>> QueryPostingPeriods();
// Command Operations
Task AddToPaymentsQueueHeaders(string customer, string checkNumber, decimal checkValue,
DateTime paymentDate, string bank, decimal trnYear, decimal trnMonth, decimal journal);
Task DeletePaymentFromQueue(string customer, string checknumber, decimal trnYear, decimal trnMonth, decimal journal);
Task DeletePaymentsFromQueue(IEnumerable<ArReversePaymentHeader> paymentHeaders);
Task ClearQueuedPayments();
// Business Operations
Task<ArReversePaymentPostCompletion> ReversePaymentsAsync(IEnumerable<ArReversePaymentHeader> checks,
IEnumerable<ArReversePaymentDetail> invoices, string postPeriod);
// Export Operations
Task ExportToExcel(ArReversePaymentPostCompletion completionObject, string filePath);
Task<IEnumerable<PostCompletion_Payment>> GetPostCompletionDetailsAsync(int trnYear, int trnMonth, int journal);
}
Service Composition and Orchestration
Complex operations through service collaboration:
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/ArReversePaymentService.cs (Lines 475-498)
public class ArReversePaymentService : IArReversePaymentService
{
private readonly ILogger<ArReversePaymentService> _logger;
private readonly ISysproPostService _sysproPostService;
private readonly IExcelExportService _excelExport;
public async Task<ArReversePaymentPostCompletion> ReversePaymentsAsync(
IEnumerable<ArReversePaymentHeader> checks,
IEnumerable<ArReversePaymentDetail> invoices,
string postPeriod)
{
ArReversePaymentPostCompletion completionObject = null;
try
{
if (checks == null || !checks.Any())
{
_logger.LogWarning("Checks enum is null or empty in ReversePaymentsAsync");
return null;
}
// Orchestrate multiple services
string inputXml = _sysproPostService.GetInputXml(checks, invoices);
string paramXml = _sysproPostService.GetParamXml(postPeriod);
string outputXml = _sysproPostService.PerformBusinessObjectPost(inputXml, paramXml);
completionObject = await BuildCompletionObjectAsync(inputXml, paramXml, outputXml);
// Log structured data
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Payment reversal completed. {@PaymentReversalContext}",
new {
checksCount = checks.Count(),
invoicesCount = invoices.Count(),
postPeriod,
success = completionObject?.PostSucceeded
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ReversePaymentsAsync");
throw;
}
return completionObject;
}
}
Service Initialization Patterns
Lazy Initialization
public class CustomFormService : ICustomFormService
{
private readonly Lazy<CustomFormConfiguration> _configuration;
private readonly ILogger<CustomFormService> _logger;
public CustomFormService(ILogger<CustomFormService> logger)
{
_logger = logger;
_configuration = new Lazy<CustomFormConfiguration>(() => LoadConfiguration());
}
private CustomFormConfiguration LoadConfiguration()
{
var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"Resources", "CustomForms.xml");
if (File.Exists(configPath))
{
var xml = XDocument.Load(configPath);
return ParseConfiguration(xml);
}
return new CustomFormConfiguration();
}
public CustomFormConfiguration Configuration => _configuration.Value;
}
Async Initialization
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/DatabaseValidationService.cs
public class DatabaseValidationService : IDatabaseValidationService
{
private bool _initialized = false;
private readonly SemaphoreSlim _initializationSemaphore = new SemaphoreSlim(1, 1);
public async Task InitializeAsync()
{
await _initializationSemaphore.WaitAsync();
try
{
if (!_initialized)
{
await LoadValidationRulesAsync();
await ValidateDatabaseSchemaAsync();
_initialized = true;
}
}
finally
{
_initializationSemaphore.Release();
}
}
}
Cross-Cutting Concerns
Logging Integration
All services implement structured logging:
public class ServiceBase
{
protected readonly ILogger Logger;
protected ServiceBase(ILogger logger)
{
Logger = logger;
}
protected T ExecuteWithLogging<T>(Func<T> operation, string operationName)
{
using (Logger.BeginScope(new { Operation = operationName }))
{
try
{
Logger.LogDebug("Starting operation {OperationName}", operationName);
var result = operation();
Logger.LogDebug("Completed operation {OperationName}", operationName);
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in operation {OperationName}", operationName);
throw;
}
}
}
}
Validation Patterns
public abstract class ValidatingService
{
protected void ValidateParameters(params (object value, string name)[] parameters)
{
foreach (var (value, name) in parameters)
{
if (value == null)
{
throw new ArgumentNullException(name);
}
if (value is string str && string.IsNullOrWhiteSpace(str))
{
throw new ArgumentException($"{name} cannot be empty", name);
}
}
}
}
Service Error Handling
Resilience Patterns
public class ResilientService
{
private readonly ILogger<ResilientService> _logger;
private readonly int _maxRetries = 3;
protected async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
int attempt = 0;
while (attempt < _maxRetries)
{
try
{
return await operation();
}
catch (TransientException ex) when (attempt < _maxRetries - 1)
{
attempt++;
_logger.LogWarning(ex, "Transient error on attempt {Attempt}, retrying...", attempt);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
}
throw new MaxRetriesExceededException($"Operation failed after {_maxRetries} attempts");
}
}
Service Testing Support
Mock-Friendly Design
// All services use interfaces for easy mocking
public interface ITestableService
{
Task<Result> PerformOperationAsync(Request request);
}
// Implementation with constructor injection
public class TestableService : ITestableService
{
private readonly IDependency _dependency;
public TestableService(IDependency dependency)
{
_dependency = dependency;
}
public async Task<Result> PerformOperationAsync(Request request)
{
return await _dependency.ProcessAsync(request);
}
}
// Easy to test
[TestMethod]
public async Task TestServiceOperation()
{
var mockDependency = new Mock<IDependency>();
mockDependency.Setup(x => x.ProcessAsync(It.IsAny<Request>()))
.ReturnsAsync(new Result { Success = true });
var service = new TestableService(mockDependency.Object);
var result = await service.PerformOperationAsync(new Request());
Assert.IsTrue(result.Success);
}
Service Performance Optimization
Caching Strategies
public class CachingService
{
private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 100
});
public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory, TimeSpan expiration)
{
if (_cache.TryGetValue(key, out T cached))
{
return cached;
}
var value = await factory();
var cacheEntry = _cache.CreateEntry(key);
cacheEntry.Value = value;
cacheEntry.Size = 1;
cacheEntry.AbsoluteExpirationRelativeToNow = expiration;
return value;
}
}
Service Registration Best Practices
- Register interfaces, not concrete types
- Use appropriate lifetimes (Singleton for stateful, Transient for stateless)
- Order matters - register dependencies before dependents
- Validate registration at startup
- Use factory patterns for complex initialization
- Document service contracts clearly
- Keep services focused on single responsibility
Common Pitfalls
- Circular dependencies between services
- Wrong service lifetime causing state issues
- Missing service registration causing runtime errors
- Synchronous blocking in async methods
- Not disposing transient services properly
Related Documentation
- Data Services - Data access layer details
- Business Logic Services - Core business services
- Integration Services - External system integration
- Utility Services - Helper and utility services
Summary
The service architecture in the AR Payment Reversal dashboard provides a robust, testable, and maintainable foundation for business logic implementation. Through proper use of dependency injection, interface segregation, and service composition patterns, the system achieves high cohesion and low coupling while maintaining flexibility for future enhancements.