Skip to main content

Service Architecture

Overview

The EFT Remittance Dashboard implements a robust service layer architecture that encapsulates business logic, data access, and external integrations. Built on the principles of dependency injection and separation of concerns, the service layer provides a maintainable, testable foundation for the application's core functionality.

Key Concepts

  • Dependency Injection: Microsoft.Extensions.DependencyInjection for IoC
  • Service Lifetimes: Singleton pattern for stateful services
  • Interface Segregation: Clear contracts between layers
  • Service Composition: Orchestration of multiple services for complex operations
  • Plugin Architecture: Integration with MepDash module services

Implementation Details

Service Registration Infrastructure

The service layer foundation is established through the RegisterServiceProvider class:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/RegisterServiceProvider.cs
internal static class RegisterServiceProvider
{
public static IServiceCollection RegisterServices(
IMepPluginServiceHandler mepPluginServiceHandler,
ISharedShellInterface sharedShellInterface)
{
IServiceCollection serviceCollection = new ServiceCollection();

// CRITICAL: Register MepDash plugin services first
serviceCollection = mepPluginServiceHandler
.RegisterMepPluginServices(serviceCollection);

// Configure SSRS reporting
var appSettings = sharedShellInterface.CurrentSession.AppConnectionSettings;
serviceCollection.AddMepReportingSsrsServerNameIp(
appSettings.SsrsReportAddress,
appSettings.SsrsReportUserName,
appSettings.SsrsReportUserPwd,
appSettings.SsrsReportUserDomain,
"",
false);

// Register application services
serviceCollection
.AddSingleton<MainViewModel>()
.AddSingleton<PluginSysproDataContext>()
.AddSingleton<IEftRemittanceService, EftRemittanceService>()
.AddSingleton<IEftRemit_RunReportsViewModel, EftRemit_RunReportsViewModel>()
.AddSingleton<EftRemit_RunReportsView>();

return serviceCollection;
}
}

Service Lifetime Management

All services in the dashboard use singleton lifetime for several reasons:

  1. State Preservation: Maintains user selections and processing state
  2. Performance: Avoids repeated initialization overhead
  3. Resource Sharing: Database contexts and connections are reused
  4. SYSPRO Integration: Aligns with SYSPRO's plugin lifecycle
// Singleton registration pattern
services.AddSingleton<IEftRemittanceService, EftRemittanceService>();
// This ensures one instance throughout the application lifetime

Dependency Injection Pattern

Services follow constructor injection for dependencies:

public class EftRemittanceService : IEftRemittanceService
{
private readonly ILogger<EftRemittanceService> _logger;
private readonly PluginSysproDataContext _context;
private readonly ISharedShellInterface _sharedShellInterface;

public EftRemittanceService(
ILogger<EftRemittanceService> logger,
PluginSysproDataContext context,
ISharedShellInterface sharedShellInterface)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_context = context ?? throw new ArgumentNullException(nameof(context));
_sharedShellInterface = sharedShellInterface
?? throw new ArgumentNullException(nameof(sharedShellInterface));
}
}

Examples

Example 1: Service Interface Definition

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/EftRemittanceService.cs
public interface IEftRemittanceService
{
// Query operations
Task<IEnumerable<SelectionItem>> QueryPayments(bool eftPaymentsOnly);
Task<IEnumerable<SelectionItem>> QueryPayments(bool eftPaymentsOnly, string checkNumber);
Task<IEnumerable<PaymentDetail>> QueryPaymentNumber(string paymentNumber, string checkFilter);

// Update operations
Task UpdateSupplierRemitEmail(string supplier, string remitEmail);

// Email operations
Task QueueMailInServiceAsync(string supplier, string paymentNumber,
string emailTo, string emailBcc, string emailSubject);
Task<int?> SendMail(string mailTo, string subject,
string fileAttachmentPath, string blindCcRecipients);

// Audit operations
Task UpdateAuditTable(string paymentNumber, string supplier,
string cheque, int? mailItemId, string emailAddress, string fileAttachmentPath);
}

Example 2: Service Composition

// Complex operation orchestrating multiple services
public class RemittanceWorkflowService
{
private readonly IEftRemittanceService _remittanceService;
private readonly IReportService _reportService;
private readonly IEmailService _emailService;
private readonly ILogger<RemittanceWorkflowService> _logger;

public async Task<WorkflowResult> ProcessRemittanceWorkflow(
PaymentDetail payment,
RemittanceOptions options)
{
try
{
// Step 1: Generate report
_logger.LogInformation("Generating report for payment {PaymentNumber}",
payment.PaymentNumber);
var reportPath = await _reportService.GenerateRemittanceReport(payment);

// Step 2: Queue email
_logger.LogInformation("Queueing email for supplier {Supplier}",
payment.Supplier);
await _remittanceService.QueueMailInServiceAsync(
payment.Supplier,
payment.PaymentNumber,
payment.RemitEmail,
options.BlindCopyList,
$"Remittance Advice - {payment.PaymentNumber}");

// Step 3: Update audit trail
_logger.LogInformation("Updating audit trail");
await _remittanceService.UpdateAuditTable(
payment.PaymentNumber,
payment.Supplier,
payment.Cheque,
null,
payment.RemitEmail,
reportPath);

return new WorkflowResult { Success = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Workflow failed for payment {PaymentNumber}",
payment.PaymentNumber);
return new WorkflowResult { Success = false, Error = ex.Message };
}
}
}

Example 3: Service Initialization Pattern

// MainView.xaml.cs - Service provider initialization
public partial class MainView : UserControl
{
private readonly IServiceProvider _mepPluginServiceProvider;
private static IServiceProvider? _mepPluginServiceProviderStatic = null;

public static IServiceProvider MepPluginServiceProvider
{
get
{
if (_mepPluginServiceProviderStatic != null)
return _mepPluginServiceProviderStatic;
throw new Exception("Service Provider not initialized");
}
}

private void InitializeServices()
{
try
{
var serviceCollection = RegisterServiceProvider.RegisterServices(
_mepPluginServiceHandler,
_sharedShellInterface);

_mepPluginServiceProvider = serviceCollection.BuildServiceProvider();
_mepPluginServiceProviderStatic = _mepPluginServiceProvider;

// Resolve and initialize view model
viewmodel = _mepPluginServiceProvider.GetService<MainViewModel>();
DataContext = viewmodel;

_logger.LogInformation("Services initialized successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize services");
throw;
}
}
}

Service Layer Patterns

Repository Pattern Implementation

While not explicitly using the repository pattern name, the data access is abstracted:

public class EftRemittanceService : IEftRemittanceService
{
public async Task<IEnumerable<SelectionItem>> QueryPayments(bool eftPaymentsOnly)
{
string sql = @"
SELECT DISTINCT
PaymentNumber as Value,
FORMAT(PaymentDate, 'yyyy-MM-dd') as Description,
PaymentDate
FROM ApRemit";

if (eftPaymentsOnly)
sql += " WHERE PaymentType = 'E'";

sql += " ORDER BY PaymentDate DESC, PaymentNumber DESC";

var payments = await _context.Database.Connection
.QueryAsync<SelectionItem>(sql)
.ConfigureAwait(true);

return payments;
}
}

Unit of Work Pattern

Transaction management is handled at the service level:

public async Task<bool> ProcessBatchRemittances(IEnumerable<PaymentDetail> payments)
{
using (var transaction = _context.Database.BeginTransaction())
{
try
{
foreach (var payment in payments)
{
await ProcessSingleRemittance(payment);
}

transaction.Commit();
return true;
}
catch
{
transaction.Rollback();
throw;
}
}
}

Service Testing Strategy

[TestClass]
public class EftRemittanceServiceTests
{
private Mock<ILogger<EftRemittanceService>> _loggerMock;
private Mock<PluginSysproDataContext> _contextMock;
private Mock<ISharedShellInterface> _shellMock;
private IEftRemittanceService _service;

[TestInitialize]
public void Setup()
{
_loggerMock = new Mock<ILogger<EftRemittanceService>>();
_contextMock = new Mock<PluginSysproDataContext>();
_shellMock = new Mock<ISharedShellInterface>();

_service = new EftRemittanceService(
_loggerMock.Object,
_contextMock.Object,
_shellMock.Object);
}

[TestMethod]
public async Task QueryPayments_WithEftOnly_ReturnsFilteredResults()
{
// Arrange
var expectedSql = "WHERE PaymentType = 'E'";

// Act
var results = await _service.QueryPayments(true);

// Assert
_contextMock.Verify(x => x.Database.Connection.QueryAsync<SelectionItem>(
It.Is<string>(s => s.Contains(expectedSql))));
}
}

Best Practices

  1. Always use interfaces for service contracts
  2. Validate dependencies in constructors with null checks
  3. Log service operations with structured logging
  4. Handle exceptions at service boundaries
  5. Use async/await for all I/O operations
  6. Implement retry logic for transient failures
  7. Cache expensive operations where appropriate
  8. Document service contracts with XML comments

Common Pitfalls

  • Not registering services before they're needed
  • Circular dependencies between services
  • Missing interface registrations
  • Incorrect service lifetimes causing state issues
  • Not disposing of resources properly

Summary

The service architecture in the EFT Remittance Dashboard provides a clean, maintainable foundation for business logic implementation. By leveraging dependency injection, interface-based contracts, and singleton lifetimes, the architecture ensures consistent behavior, testability, and integration with the broader SYSPRO ecosystem. The service layer acts as the crucial bridge between the presentation layer and data access, orchestrating complex business operations while maintaining separation of concerns.