Skip to main content

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

  1. Register interfaces, not concrete types
  2. Use appropriate lifetimes (Singleton for stateful, Transient for stateless)
  3. Order matters - register dependencies before dependents
  4. Validate registration at startup
  5. Use factory patterns for complex initialization
  6. Document service contracts clearly
  7. Keep services focused on single responsibility

Common Pitfalls

  1. Circular dependencies between services
  2. Wrong service lifetime causing state issues
  3. Missing service registration causing runtime errors
  4. Synchronous blocking in async methods
  5. Not disposing transient services properly

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.