Skip to main content

Integration Services

Overview

The integration services layer in the EFT Remittance Dashboard manages all external system communications, including SSRS report generation, email service integration, and network file system operations. This layer provides abstraction over external dependencies, ensuring the application remains resilient to external service failures while maintaining clean separation between business logic and infrastructure concerns.

Key Concepts

  • External Service Abstraction: Isolates external dependencies
  • Resilience Patterns: Circuit breakers and retry mechanisms
  • Service Authentication: Secure credential management
  • Data Transformation: Mapping between internal and external formats
  • Error Mapping: Consistent error handling across services

Implementation Details

SSRS Integration Service

The report server integration handles all SSRS communications:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/ReportServerService.cs
public class ReportServerService : IReportServerService
{
private readonly ILogger<ReportServerService> _logger;
private readonly ISharedShellInterface _sharedShellInterface;
private readonly HttpClient _httpClient;

public ReportServerService(
ILogger<ReportServerService> logger,
ISharedShellInterface sharedShellInterface,
IHttpClientFactory httpClientFactory)
{
_logger = logger;
_sharedShellInterface = sharedShellInterface;
_httpClient = httpClientFactory.CreateClient("SSRS");

ConfigureHttpClient();
}

private void ConfigureHttpClient()
{
var settings = _sharedShellInterface.CurrentSession.AppConnectionSettings;

_httpClient.BaseAddress = new Uri(settings.SsrsReportAddress);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes(
$"{settings.SsrsReportUserName}:{settings.SsrsReportUserPwd}")));

_httpClient.Timeout = TimeSpan.FromMinutes(5); // Reports can take time
}

public async Task<ReportResult> RenderReportAsync(
string reportPath,
IEnumerable<ReportParameter> parameters,
string format = "PDF")
{
try
{
_logger.LogDebug("Rendering report {ReportPath} as {Format}", reportPath, format);

var url = BuildReportUrl(reportPath, parameters, format);

using (var response = await _httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();

var content = await response.Content.ReadAsByteArrayAsync();

_logger.LogInformation("Report rendered successfully. Size: {Size} bytes",
content.Length);

return new ReportResult
{
Success = true,
Content = content,
ContentType = response.Content.Headers.ContentType?.ToString(),
FileName = GetFileNameFromHeaders(response.Headers)
};
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error rendering report {ReportPath}", reportPath);
throw new IntegrationException($"Failed to connect to report server: {ex.Message}", ex);
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Report rendering timeout for {ReportPath}", reportPath);
throw new IntegrationException("Report generation timed out", ex);
}
}

private string BuildReportUrl(
string reportPath,
IEnumerable<ReportParameter> parameters,
string format)
{
var builder = new UriBuilder(_httpClient.BaseAddress)
{
Path = $"/ReportServer?{reportPath}"
};

var query = HttpUtility.ParseQueryString(string.Empty);
query["rs:Format"] = format;
query["rs:Command"] = "Render";

foreach (var param in parameters)
{
query[param.Name] = param.Value?.ToString();
}

builder.Query = query.ToString();
return builder.ToString();
}
}

Email Service Integration

The email service manages all email communications:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/EmailIntegrationService.cs
public class EmailIntegrationService : IEmailIntegrationService
{
private readonly ILogger<EmailIntegrationService> _logger;
private readonly HttpClient _httpClient;
private readonly ISharedShellInterface _sharedShellInterface;
private readonly CircuitBreaker _circuitBreaker;

public EmailIntegrationService(
ILogger<EmailIntegrationService> logger,
IHttpClientFactory httpClientFactory,
ISharedShellInterface sharedShellInterface)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient("EmailService");
_sharedShellInterface = sharedShellInterface;
_circuitBreaker = new CircuitBreaker(
failureThreshold: 5,
recoveryTimeout: TimeSpan.FromMinutes(1));
}

public async Task<int?> SendEmailAsync(EmailMessage message)
{
return await _circuitBreaker.ExecuteAsync(async () =>
{
try
{
var request = new EmailRequest
{
To = message.To,
Cc = message.Cc,
Bcc = message.Bcc,
Subject = message.Subject,
Body = message.Body,
IsHtml = message.IsHtml,
Attachments = await PrepareAttachments(message.Attachments),
Priority = MapPriority(message.Priority),
Headers = new Dictionary<string, string>
{
["X-Syspro-Company"] = _sharedShellInterface.CurrentSession.SysproCompany,
["X-Syspro-Operator"] = _sharedShellInterface.CurrentSession.SysproOperator,
["X-Application"] = "EFT-Remittance"
}
};

var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await _httpClient.PostAsync("/api/email/send", content);

if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
var emailResult = JsonSerializer.Deserialize<EmailSendResult>(result);

_logger.LogInformation("Email sent successfully. MailId: {MailId}",
emailResult.MailId);

return emailResult.MailId;
}
else
{
var error = await response.Content.ReadAsStringAsync();
_logger.LogError("Email service returned error: {StatusCode} - {Error}",
response.StatusCode, error);

if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RateLimitException("Email service rate limit exceeded");
}

throw new IntegrationException($"Email service error: {response.StatusCode}");
}
}
catch (Exception ex) when (!(ex is IntegrationException))
{
_logger.LogError(ex, "Unexpected error sending email");
throw new IntegrationException("Failed to send email", ex);
}
});
}

private async Task<List<EmailAttachment>> PrepareAttachments(string[] filePaths)
{
var attachments = new List<EmailAttachment>();

foreach (var path in filePaths ?? Array.Empty<string>())
{
if (!File.Exists(path))
{
_logger.LogWarning("Attachment file not found: {Path}", path);
continue;
}

var fileInfo = new FileInfo(path);
if (fileInfo.Length > MaxAttachmentSize)
{
_logger.LogWarning("Attachment too large: {Path} ({Size} bytes)",
path, fileInfo.Length);
continue;
}

attachments.Add(new EmailAttachment
{
FileName = fileInfo.Name,
ContentType = GetMimeType(fileInfo.Extension),
Content = Convert.ToBase64String(await File.ReadAllBytesAsync(path))
});
}

return attachments;
}
}

Examples

Example 1: Retry Pattern Implementation

public class RetryPolicy
{
private readonly int _maxAttempts;
private readonly TimeSpan _delay;
private readonly ILogger _logger;

public RetryPolicy(int maxAttempts, TimeSpan delay, ILogger logger)
{
_maxAttempts = maxAttempts;
_delay = delay;
_logger = logger;
}

public async Task<T> ExecuteAsync<T>(
Func<Task<T>> operation,
Func<Exception, bool> shouldRetry)
{
for (int attempt = 1; attempt <= _maxAttempts; attempt++)
{
try
{
return await operation();
}
catch (Exception ex) when (attempt < _maxAttempts && shouldRetry(ex))
{
var delayMs = _delay.TotalMilliseconds * Math.Pow(2, attempt - 1);

_logger.LogWarning(ex,
"Operation failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms",
attempt, _maxAttempts, delayMs);

await Task.Delay(TimeSpan.FromMilliseconds(delayMs));
}
}

// Final attempt without catching
return await operation();
}
}

// Usage
var retryPolicy = new RetryPolicy(3, TimeSpan.FromSeconds(1), _logger);

var result = await retryPolicy.ExecuteAsync(
async () => await _emailService.SendEmailAsync(message),
ex => ex is HttpRequestException || ex is TaskCanceledException);

Example 2: Circuit Breaker Pattern

public class CircuitBreaker
{
private readonly int _failureThreshold;
private readonly TimeSpan _recoveryTimeout;
private int _failureCount;
private DateTime _lastFailureTime;
private CircuitState _state = CircuitState.Closed;

public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow - _lastFailureTime > _recoveryTimeout)
{
_state = CircuitState.HalfOpen;
}
else
{
throw new CircuitBreakerOpenException(
$"Circuit breaker is open. Recovery at {_lastFailureTime.Add(_recoveryTimeout)}");
}
}

try
{
var result = await operation();

if (_state == CircuitState.HalfOpen)
{
_state = CircuitState.Closed;
_failureCount = 0;
}

return result;
}
catch (Exception ex)
{
_failureCount++;
_lastFailureTime = DateTime.UtcNow;

if (_failureCount >= _failureThreshold)
{
_state = CircuitState.Open;
throw new CircuitBreakerOpenException(
"Circuit breaker opened due to repeated failures", ex);
}

throw;
}
}
}

Example 3: Network File System Integration

public class NetworkFileService : INetworkFileService
{
private readonly ILogger<NetworkFileService> _logger;
private readonly NetworkCredential _credential;

public async Task<string> SaveToNetworkShareAsync(
string networkPath,
string fileName,
byte[] content)
{
try
{
// Impersonate if credentials provided
using (var impersonation = _credential != null
? new NetworkImpersonation(_credential)
: null)
{
// Ensure directory exists
var directory = Path.GetDirectoryName(Path.Combine(networkPath, fileName));
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
_logger.LogDebug("Created directory: {Directory}", directory);
}

// Generate unique filename if exists
var fullPath = GetUniqueFilePath(networkPath, fileName);

// Write with retry for network issues
await WriteFileWithRetryAsync(fullPath, content);

// Verify file was written
var fileInfo = new FileInfo(fullPath);
if (fileInfo.Length != content.Length)
{
throw new IntegrationException(
$"File size mismatch. Expected {content.Length}, got {fileInfo.Length}");
}

_logger.LogInformation("File saved to network share: {Path}", fullPath);
return fullPath;
}
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Access denied to network path: {Path}", networkPath);
throw new IntegrationException("Network share access denied", ex);
}
catch (IOException ex)
{
_logger.LogError(ex, "IO error accessing network share: {Path}", networkPath);
throw new IntegrationException("Network share IO error", ex);
}
}

private async Task WriteFileWithRetryAsync(string path, byte[] content)
{
const int maxAttempts = 3;

for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
await File.WriteAllBytesAsync(path, content);
return;
}
catch (IOException ex) when (attempt < maxAttempts && IsTransientError(ex))
{
_logger.LogWarning(ex, "Transient error writing file, attempt {Attempt}", attempt);
await Task.Delay(TimeSpan.FromSeconds(attempt));
}
}
}
}

Authentication and Authorization

Service Authentication Management

public class ServiceAuthenticationManager
{
private readonly IConfiguration _configuration;
private readonly ILogger<ServiceAuthenticationManager> _logger;
private readonly Dictionary<string, TokenInfo> _tokenCache = new();

public async Task<string> GetTokenAsync(string service)
{
if (_tokenCache.TryGetValue(service, out var tokenInfo) &&
tokenInfo.ExpiresAt > DateTime.UtcNow.AddMinutes(5))
{
return tokenInfo.Token;
}

var token = await RefreshTokenAsync(service);
_tokenCache[service] = token;
return token.Token;
}

private async Task<TokenInfo> RefreshTokenAsync(string service)
{
var config = _configuration.GetSection($"Services:{service}");

var request = new TokenRequest
{
ClientId = config["ClientId"],
ClientSecret = config["ClientSecret"],
Scope = config["Scope"],
GrantType = "client_credentials"
};

// Request new token from auth service
var response = await _authClient.RequestTokenAsync(request);

_logger.LogInformation("Token refreshed for service {Service}", service);

return new TokenInfo
{
Token = response.AccessToken,
ExpiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn)
};
}
}

Error Handling and Mapping

public class IntegrationErrorHandler
{
private readonly ILogger<IntegrationErrorHandler> _logger;

public async Task<T> HandleIntegrationCall<T>(
Func<Task<T>> operation,
string context)
{
try
{
return await operation();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Network error in {Context}", context);
throw new IntegrationException($"Network error: {ex.Message}", ex);
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Timeout in {Context}", context);
throw new IntegrationException("Operation timed out", ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Authorization failed in {Context}", context);
throw new IntegrationException("Authorization failed", ex);
}
catch (IntegrationException)
{
throw; // Already handled
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in {Context}", context);
throw new IntegrationException($"Unexpected error: {ex.Message}", ex);
}
}
}

Best Practices

  1. Use circuit breakers for external service calls
  2. Implement retry policies with exponential backoff
  3. Cache authentication tokens to reduce auth calls
  4. Log all integration points with structured logging
  5. Transform external errors to domain-specific exceptions
  6. Validate external responses before processing
  7. Use health checks to monitor service availability
  8. Implement timeouts for all external calls

Common Pitfalls

  • Not handling transient network failures
  • Missing timeout configurations
  • Exposing external service details in error messages
  • Not validating SSL certificates in production
  • Inadequate logging for troubleshooting
  • Missing circuit breaker recovery mechanisms

Summary

The integration services layer provides a robust, resilient interface to external systems required by the EFT Remittance Dashboard. Through careful implementation of retry policies, circuit breakers, and error handling, the system maintains stability even when external services experience issues. The abstraction layer ensures that business logic remains isolated from infrastructure concerns while providing comprehensive logging and monitoring capabilities for production support.