Skip to main content

Utility Services

Overview

The utility services layer provides cross-cutting functionality and helper services that support the entire EFT Remittance Dashboard application. These services handle common tasks such as logging configuration, resource management, validation utilities, and data formatting, ensuring consistent behavior across all components while reducing code duplication.

Key Concepts

  • Cross-Cutting Concerns: Functionality used across multiple layers
  • Helper Functions: Reusable utility methods
  • Resource Management: Handling of embedded resources and external files
  • Validation Utilities: Common validation logic
  • Data Formatting: Consistent formatting across the application

Implementation Details

Logging Service Configuration

The application uses Serilog with structured logging:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/LoggingService.cs
public static class LoggingService
{
public static ILogger ConfigureLogging(ISharedShellInterface sharedShellInterface)
{
var logPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MepApps",
"Logs",
"EftRemittance",
$"log-{DateTime.Now:yyyy-MM-dd}.txt");

var loggerConfiguration = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.Enrich.WithProperty("Application", "EftRemittance")
.Enrich.WithProperty("Company", sharedShellInterface?.CurrentSession?.SysproCompany)
.Enrich.WithProperty("Operator", sharedShellInterface?.CurrentSession?.SysproOperator);

// File sink
loggerConfiguration.WriteTo.File(
logPath,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 10_485_760, // 10MB
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}");

// Seq sink if configured
var seqUrl = sharedShellInterface?.CurrentSession?.AppConnectionSettings?.SeqServerUrl;
if (!string.IsNullOrWhiteSpace(seqUrl))
{
loggerConfiguration.WriteTo.Seq(
seqUrl,
apiKey: sharedShellInterface.CurrentSession.AppConnectionSettings.SeqApiKey,
restrictedToMinimumLevel: LogEventLevel.Information);
}

// Debug output in development
#if DEBUG
loggerConfiguration.WriteTo.Debug(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}");
#endif

Log.Logger = loggerConfiguration.CreateLogger();

Log.Information("Logging initialized. {@LogConfiguration}",
new { logPath, seqUrl = !string.IsNullOrWhiteSpace(seqUrl) });

return Log.Logger;
}
}

Validation Helper Service

Common validation utilities used throughout the application:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/ValidationHelper.cs
public static class ValidationHelper
{
private static readonly Regex EmailRegex = new Regex(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);

private static readonly Regex SupplierCodeRegex = new Regex(
@"^[A-Z0-9]{1,15}$",
RegexOptions.Compiled);

public static bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;

if (email.Length > 255)
return false;

return EmailRegex.IsMatch(email);
}

public static bool IsValidSupplierCode(string code)
{
if (string.IsNullOrWhiteSpace(code))
return false;

return SupplierCodeRegex.IsMatch(code);
}

public static ValidationResult ValidatePaymentAmount(decimal amount)
{
var errors = new List<string>();

if (amount <= 0)
errors.Add("Payment amount must be positive");

if (amount > 999999999.99m)
errors.Add("Payment amount exceeds maximum allowed value");

// Check decimal places
var decimalPlaces = BitConverter.GetBytes(decimal.GetBits(amount)[3])[2];
if (decimalPlaces > 2)
errors.Add("Payment amount cannot have more than 2 decimal places");

return new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors
};
}

public static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return "document";

var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = string.Join("_", fileName.Split(invalidChars));

// Limit length
if (sanitized.Length > 255)
sanitized = sanitized.Substring(0, 255);

return sanitized;
}
}

Resource Helper

Manages embedded resources and external files:

// MepApps.Dash.Ap.Rpt.EftRemittance/Services/ResourceHelper.cs
public class ResourceHelper
{
private readonly Assembly _assembly;
private readonly ILogger<ResourceHelper> _logger;

public ResourceHelper(ILogger<ResourceHelper> logger)
{
_assembly = Assembly.GetExecutingAssembly();
_logger = logger;
}

public Stream GetEmbeddedResource(string resourceName)
{
var fullResourceName = $"{_assembly.GetName().Name}.Resources.{resourceName}";

var stream = _assembly.GetManifestResourceStream(fullResourceName);

if (stream == null)
{
_logger.LogError("Embedded resource not found: {ResourceName}", fullResourceName);
throw new ResourceNotFoundException($"Resource '{resourceName}' not found");
}

_logger.LogDebug("Loaded embedded resource: {ResourceName}", resourceName);
return stream;
}

public string GetResourceAsString(string resourceName)
{
using var stream = GetEmbeddedResource(resourceName);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}

public byte[] GetResourceAsBytes(string resourceName)
{
using var stream = GetEmbeddedResource(resourceName);
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}

public T LoadResourceAsXml<T>(string resourceName) where T : class
{
var xml = GetResourceAsString(resourceName);
var serializer = new XmlSerializer(typeof(T));

using var reader = new StringReader(xml);
return serializer.Deserialize(reader) as T;
}

public T LoadResourceAsJson<T>(string resourceName) where T : class
{
var json = GetResourceAsString(resourceName);
return JsonSerializer.Deserialize<T>(json);
}
}

Examples

Example 1: Data Formatting Utilities

public static class FormatHelper
{
public static string FormatCurrency(decimal amount, string currencyCode = "USD")
{
var culture = currencyCode switch
{
"USD" => new CultureInfo("en-US"),
"EUR" => new CultureInfo("fr-FR"),
"GBP" => new CultureInfo("en-GB"),
"ZAR" => new CultureInfo("en-ZA"),
_ => CultureInfo.CurrentCulture
};

return amount.ToString("C", culture);
}

public static string FormatPaymentNumber(string paymentNumber)
{
if (string.IsNullOrWhiteSpace(paymentNumber))
return string.Empty;

// Format as PMT-YYYY-NNNNN
if (paymentNumber.Length >= 9 && !paymentNumber.Contains("-"))
{
return $"PMT-{paymentNumber.Substring(0, 4)}-{paymentNumber.Substring(4)}";
}

return paymentNumber;
}

public static string FormatSupplierName(string name, int maxLength = 50)
{
if (string.IsNullOrWhiteSpace(name))
return string.Empty;

name = name.Trim();

if (name.Length > maxLength)
{
return name.Substring(0, maxLength - 3) + "...";
}

return name;
}

public static string FormatDateRange(DateTime startDate, DateTime endDate)
{
if (startDate.Date == endDate.Date)
return startDate.ToString("MMMM d, yyyy");

if (startDate.Year == endDate.Year && startDate.Month == endDate.Month)
return $"{startDate:MMMM d}-{endDate:d, yyyy}";

if (startDate.Year == endDate.Year)
return $"{startDate:MMMM d} - {endDate:MMMM d, yyyy}";

return $"{startDate:MMMM d, yyyy} - {endDate:MMMM d, yyyy}";
}
}

Example 2: Configuration Helper

public class ConfigurationHelper
{
private readonly IConfiguration _configuration;
private readonly ILogger<ConfigurationHelper> _logger;

public ConfigurationHelper(
IConfiguration configuration,
ILogger<ConfigurationHelper> logger)
{
_configuration = configuration;
_logger = logger;
}

public T GetSetting<T>(string key, T defaultValue = default)
{
try
{
var value = _configuration[key];

if (string.IsNullOrWhiteSpace(value))
{
_logger.LogDebug("Setting {Key} not found, using default: {Default}",
key, defaultValue);
return defaultValue;
}

if (typeof(T) == typeof(string))
{
return (T)(object)value;
}

if (typeof(T) == typeof(int))
{
return (T)(object)int.Parse(value);
}

if (typeof(T) == typeof(bool))
{
return (T)(object)bool.Parse(value);
}

if (typeof(T) == typeof(decimal))
{
return (T)(object)decimal.Parse(value);
}

if (typeof(T) == typeof(TimeSpan))
{
return (T)(object)TimeSpan.Parse(value);
}

// For complex types, deserialize from JSON
return JsonSerializer.Deserialize<T>(value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading setting {Key}", key);
return defaultValue;
}
}

public Dictionary<string, string> GetSection(string section)
{
var configSection = _configuration.GetSection(section);

if (!configSection.Exists())
{
_logger.LogWarning("Configuration section {Section} not found", section);
return new Dictionary<string, string>();
}

return configSection.GetChildren()
.ToDictionary(x => x.Key, x => x.Value);
}
}

Example 3: Cryptography Helper

public static class CryptoHelper
{
private static readonly byte[] Salt = Encoding.UTF8.GetBytes("MepApps.EftRemittance.2024");

public static string Encrypt(string plainText, string password)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;

using var aes = Aes.Create();
using var key = new Rfc2898DeriveBytes(password, Salt, 10000);

aes.Key = key.GetBytes(32);
aes.IV = key.GetBytes(16);

using var encryptor = aes.CreateEncryptor();
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);

return Convert.ToBase64String(cipherBytes);
}

public static string Decrypt(string cipherText, string password)
{
if (string.IsNullOrEmpty(cipherText))
return string.Empty;

using var aes = Aes.Create();
using var key = new Rfc2898DeriveBytes(password, Salt, 10000);

aes.Key = key.GetBytes(32);
aes.IV = key.GetBytes(16);

using var decryptor = aes.CreateDecryptor();
var cipherBytes = Convert.FromBase64String(cipherText);
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);

return Encoding.UTF8.GetString(plainBytes);
}

public static string HashPassword(string password)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(password + Convert.ToBase64String(Salt));
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}

Service Extension Methods

Extension methods that enhance existing services:

public static class ServiceExtensions
{
public static IServiceCollection AddUtilityServices(this IServiceCollection services)
{
services.AddSingleton<ResourceHelper>();
services.AddSingleton<ConfigurationHelper>();
services.AddSingleton<IMemoryCache, MemoryCache>();

// Register HTTP clients with policies
services.AddHttpClient("Default")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());

return services;
}

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
var logger = context.Values["logger"] as ILogger;
logger?.LogWarning("Retry {RetryCount} after {Delay}ms",
retryCount, timespan.TotalMilliseconds);
});
}

private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
5,
TimeSpan.FromSeconds(30),
onBreak: (result, timespan) =>
{
Log.Warning("Circuit breaker opened for {Duration}s", timespan.TotalSeconds);
},
onReset: () =>
{
Log.Information("Circuit breaker reset");
});
}
}

Performance Utilities

public class PerformanceMonitor
{
private readonly ILogger<PerformanceMonitor> _logger;
private readonly Dictionary<string, Stopwatch> _timers = new();

public IDisposable MeasureOperation(string operation)
{
return new OperationTimer(operation, _logger);
}

private class OperationTimer : IDisposable
{
private readonly string _operation;
private readonly ILogger _logger;
private readonly Stopwatch _stopwatch;

public OperationTimer(string operation, ILogger logger)
{
_operation = operation;
_logger = logger;
_stopwatch = Stopwatch.StartNew();
}

public void Dispose()
{
_stopwatch.Stop();

if (_stopwatch.ElapsedMilliseconds > 1000)
{
_logger.LogWarning("Slow operation {Operation}: {ElapsedMs}ms",
_operation, _stopwatch.ElapsedMilliseconds);
}
else
{
_logger.LogDebug("Operation {Operation} completed in {ElapsedMs}ms",
_operation, _stopwatch.ElapsedMilliseconds);
}
}
}
}

Best Practices

  1. Use extension methods to enhance existing types
  2. Implement caching for expensive operations
  3. Provide default values for configuration settings
  4. Use compiled regex for performance
  5. Log utility operations at appropriate levels
  6. Handle null inputs gracefully
  7. Make utilities testable through dependency injection
  8. Document utility methods with XML comments

Common Pitfalls

  • Not handling culture-specific formatting
  • Missing null checks in utility methods
  • Inefficient string operations in loops
  • Not disposing of resources properly
  • Inadequate error handling in helpers
  • Hard-coding configuration values

Summary

The utility services layer provides essential support functionality that enhances the reliability, maintainability, and consistency of the EFT Remittance Dashboard. Through careful implementation of logging, validation, formatting, and resource management utilities, the application maintains high quality standards while reducing code duplication. These utilities form the foundation upon which the business logic and presentation layers build their functionality, ensuring consistent behavior throughout the application.