Skip to main content

Utility Services

Overview

The utility services layer in MepApps.Dash.Inv.Batch.MiniMrpOrderCreation provides essential supporting functionality including configuration management, database validation, resource handling, logging integration, and various helper services that support the core business operations. These services handle cross-cutting concerns and provide reusable functionality across the application.

Key Concepts

  • Configuration Management: MepSettings service for dynamic configuration
  • Database Validation: Ensuring database schema integrity
  • Resource Management: Embedded resource handling
  • View Factory: Dynamic view creation and lifecycle management
  • Excel Export: Data export capabilities

Implementation Details

MepSettings Service

Dynamic configuration management with scope-based settings:

// Services/MepSettingsService.cs (lines 19-125)
public class MepSettingsService : IMepSettingsService
{
private readonly ILogger<IMepSettingsService> _logger;
private readonly ISysproTenantPreferenceManager _sysproTenantPreferenceManager;

public List<MepSettings> Settings { get; private set; } = new List<MepSettings>();

public async Task InitializeDefaultAsync()
{
try
{
string settingsJson = GetResourceFile();
if (string.IsNullOrEmpty(settingsJson))
{
_logger.LogError("Failed to load settings");
throw new Exception("Failed to load settings");
}

Settings = JsonConvert.DeserializeObject<List<MepSettings>>(settingsJson);

foreach (var setting in Settings.Where(x => !string.IsNullOrWhiteSpace(x.SettingName)))
{
_logger.LogInformation("Settings defaults for SettingName: {SettingName}",
setting.SettingName);

// Retrieve current scope
var currentSettingScope = await _sysproTenantPreferenceManager
.GetByProgramAsync(setting.SettingName + "SCOPE")
.ConfigureAwait(true);

if (string.IsNullOrWhiteSpace(currentSettingScope))
{
if (string.IsNullOrWhiteSpace(setting.SettingScope))
{
setting.SettingScope = "Program";
}
await _sysproTenantPreferenceManager
.SetByProgramAsync(setting.SettingName + "SCOPE", setting.SettingScope)
.ConfigureAwait(true);
}
else
{
setting.SettingScope = currentSettingScope;
}

// Retrieve or set value based on scope
string currentSettingValue = string.Empty;
switch (setting.SettingScope.Trim().ToUpper())
{
case "OPERATOR":
currentSettingValue = await _sysproTenantPreferenceManager
.GetByOperatorAsync(setting.SettingName).ConfigureAwait(true);
break;
case "COMPANY":
currentSettingValue = await _sysproTenantPreferenceManager
.GetByCompanyAsync(setting.SettingName).ConfigureAwait(true);
break;
case "COMPANYOPERATOR":
currentSettingValue = await _sysproTenantPreferenceManager
.GetByCompanyOperatorAsync(setting.SettingName).ConfigureAwait(true);
break;
default:
currentSettingValue = await _sysproTenantPreferenceManager
.GetByProgramAsync(setting.SettingName).ConfigureAwait(true);
break;
}

if (string.IsNullOrEmpty(currentSettingValue))
{
// Set default value
await SetValueByScopeAsync(setting.SettingName,
setting.SettingValue, setting.SettingScope);
}
else
{
setting.SettingValue = currentSettingValue;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load settings");
throw;
}
}
}

Database Validation Service

Ensuring database schema integrity and managing custom views:

// Services/DatabaseValidationService.cs (lines 18-157)
public class DatabaseValidationService : IDatabaseValidationService
{
private readonly ILogger<DatabaseValidationService> _logger;
private readonly PluginSysproDataContext _pluginSysproDataContext;

public List<DatabaseValidationObject> DatabaseValidationObjects { get; private set; }

public async Task InitializeAsync()
{
try
{
string validationJson = ResourceHelper.GetResourceFileText("DatabaseValidation.json");
DatabaseValidationObjects = JsonConvert
.DeserializeObject<List<DatabaseValidationObject>>(validationJson);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in InitializeAsync");
throw;
}
}

public async Task CreateMissingDatabaseViewsAsync()
{
try
{
foreach (var view in DatabaseValidationObjects.Where(
x => x.ObjectType.Trim().ToUpper() == "VIEW" &&
!string.IsNullOrWhiteSpace(x.Name)))
{
await CreateViewIfNotExistsSysproAsync(view.Name, view.ResourceFile)
.ConfigureAwait(true);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in CreateMissingDatabaseViewsAsync");
}
}

public async Task CreateViewIfNotExistsSysproAsync(
string viewName,
string scriptResource)
{
try
{
_logger.LogInformation($"Checking if Syspro database view exists: {viewName}");

if (await ViewExistsSysproAsync(viewName).ConfigureAwait(true))
{
_logger.LogInformation($"View exists in Syspro database: {viewName}");
}
else
{
_logger.LogInformation($"View not found. Creating from script resource.");
await RunScriptResourceSysproAsync(scriptResource).ConfigureAwait(true);

if (await ViewExistsSysproAsync(viewName).ConfigureAwait(true))
{
_logger.LogWarning($"Create view completed for: {viewName}");
}
else
{
_logger.LogWarning($"Create view failed for: {viewName}");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in CreateViewIfNotExistsSysproAsync");
}
}

public async Task<bool> ViewExistsSysproAsync(string viewName)
{
string sSql = $"IF EXISTS(select * FROM sys.views where name = '{viewName}') " +
"select cast(1 as bit) else select cast(0 as bit)";
try
{
return await _pluginSysproDataContext.Database.Connection
.QueryFirstOrDefaultAsync<bool>(sSql).ConfigureAwait(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ViewExistsSysproAsync");
throw;
}
}
}

Resource Helper

Managing embedded resources:

// Utilities/ResourceHelper.cs
public static class ResourceHelper
{
private static readonly Assembly _assembly = Assembly.GetExecutingAssembly();
private static readonly string _namespace = "MepApps.Dash.Inv.Batch.MiniMrpOrderCreation";

public static string GetResourceFileText(string resourceName)
{
try
{
var fullResourceName = $"{_namespace}.Resources.{resourceName}";

using var stream = _assembly.GetManifestResourceStream(fullResourceName);
if (stream == null)
{
throw new FileNotFoundException(
$"Resource {fullResourceName} not found in assembly");
}

using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
catch (Exception ex)
{
throw new ResourceException(
$"Failed to load resource {resourceName}", ex);
}
}

public static byte[] GetResourceFileBytes(string resourceName)
{
try
{
var fullResourceName = $"{_namespace}.Resources.{resourceName}";

using var stream = _assembly.GetManifestResourceStream(fullResourceName);
if (stream == null)
{
throw new FileNotFoundException(
$"Resource {fullResourceName} not found");
}

using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
catch (Exception ex)
{
throw new ResourceException(
$"Failed to load resource bytes {resourceName}", ex);
}
}

public static IEnumerable<string> GetResourceNames(string folder = null)
{
var prefix = string.IsNullOrEmpty(folder)
? $"{_namespace}.Resources."
: $"{_namespace}.Resources.{folder}.";

return _assembly.GetManifestResourceNames()
.Where(name => name.StartsWith(prefix))
.Select(name => name.Substring(prefix.Length));
}
}

View Factory Service

Dynamic view creation and management:

// Services/ViewFactoryService.cs
public class ViewFactoryService : IViewFactoryService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ViewFactoryService> _logger;

public ViewFactoryService(
IServiceProvider serviceProvider,
ILogger<ViewFactoryService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}

public T CreateView<T>() where T : UserControl
{
try
{
var view = _serviceProvider.GetService<T>();
if (view == null)
{
throw new InvalidOperationException(
$"View type {typeof(T).Name} is not registered");
}

InitializeView(view);
return view;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create view {ViewType}", typeof(T).Name);
throw;
}
}

private void InitializeView(UserControl view)
{
try
{
// Set DataContext if ViewModel exists
var viewModelType = GetViewModelType(view.GetType());
if (viewModelType != null)
{
var viewModel = _serviceProvider.GetService(viewModelType);
view.DataContext = viewModel;

// Initialize ViewModel if it implements IAsyncInitializable
if (viewModel is IAsyncInitializable asyncInit)
{
Task.Run(async () =>
{
await asyncInit.InitializeAsync();
});
}
}

// Subscribe to view events
view.Loaded += OnViewLoaded;
view.Unloaded += OnViewUnloaded;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize view");
throw;
}
}

private Type GetViewModelType(Type viewType)
{
var viewName = viewType.Name;
var viewModelName = viewName.EndsWith("View")
? viewName + "Model"
: viewName + "ViewModel";

var viewModelType = viewType.Assembly.GetTypes()
.FirstOrDefault(t => t.Name == viewModelName);

return viewModelType;
}
}

Paged Data Service

Managing large datasets with pagination:

// Services/PagedDataViewModel.cs
public class PagedDataViewModel<T> : IPagedDataViewModel<T> where T : class
{
private readonly ILogger<PagedDataViewModel<T>> _logger;
private IQueryable<T> _sourceQuery;
private List<T> _currentPageData;

public int PageSize { get; set; } = 50;
public int CurrentPage { get; set; } = 1;
public int TotalPages { get; private set; }
public int TotalRecords { get; private set; }

public IEnumerable<T> CurrentPageData => _currentPageData;

public void RegisterPagedDataViewModel(Func<IQueryable<T>> queryFactory)
{
try
{
_sourceQuery = queryFactory();
TotalRecords = _sourceQuery.Count();
TotalPages = (int)Math.Ceiling(TotalRecords / (double)PageSize);
LoadPage(1);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register paged data");
throw;
}
}

public void LoadPage(int pageNumber)
{
try
{
if (pageNumber < 1 || pageNumber > TotalPages)
{
throw new ArgumentOutOfRangeException(nameof(pageNumber));
}

CurrentPage = pageNumber;
var skipCount = (pageNumber - 1) * PageSize;

_currentPageData = _sourceQuery
.Skip(skipCount)
.Take(PageSize)
.ToList();

OnPropertyChanged(nameof(CurrentPageData));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load page {PageNumber}", pageNumber);
throw;
}
}

public RelayCommandAsync FirstPageCmd =>
new RelayCommandAsync(() => Task.Run(() => LoadPage(1)));

public RelayCommandAsync PreviousPageCmd =>
new RelayCommandAsync(() => Task.Run(() => LoadPage(CurrentPage - 1)));

public RelayCommandAsync NextPageCmd =>
new RelayCommandAsync(() => Task.Run(() => LoadPage(CurrentPage + 1)));

public RelayCommandAsync LastPageCmd =>
new RelayCommandAsync(() => Task.Run(() => LoadPage(TotalPages)));
}

Excel Export Service

Data export functionality:

// Services/ExcelExportService.cs
public class ExcelExportService : IExcelExportService
{
private readonly ILogger<ExcelExportService> _logger;

public void ExportToExcel<T>(
IEnumerable<T> data,
string fileName,
string worksheetName = "Data") where T : class
{
try
{
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add(worksheetName);

// Load data with headers
worksheet.Cells["A1"].LoadFromCollection(data, true);

// Format as table
if (worksheet.Dimension != null)
{
var table = worksheet.Tables.Add(
worksheet.Dimension,
worksheetName.Replace(" ", ""));
table.TableStyle = TableStyles.Medium2;

// Auto-fit columns
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();

// Format dates
FormatDateColumns(worksheet, typeof(T));

// Format numbers
FormatNumberColumns(worksheet, typeof(T));
}

// Save file
var file = new FileInfo(fileName);
package.SaveAs(file);

_logger.LogInformation("Exported {Count} records to {FileName}",
data.Count(), fileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to export to Excel");
throw;
}
}
}

Examples

Example 1: Settings Usage

// Using MepSettings in business logic
public class OrderService
{
private readonly IMepSettingsService _settings;

public SummaryOrder CreateOrderSummary(IEnumerable<OrderLine> lines)
{
// Get configuration values
bool orderByWarehouse = _settings.GetBool("OrderByWarehouse");
bool orderByDueDate = _settings.GetBool("OrderByDueDate");
int maxLinesPerOrder = _settings.GetInt("MaxLinesPerOrder", 100);

// Apply settings to business logic
var grouped = orderByWarehouse
? lines.GroupBy(l => new { l.Supplier, l.Warehouse })
: lines.GroupBy(l => l.Supplier);

// Further processing...
}
}

Example 2: Database View Validation

// Ensuring custom views exist
public class StartupService
{
private readonly IDatabaseValidationService _dbValidation;

public async Task InitializeAsync()
{
// Load validation configuration
await _dbValidation.InitializeAsync();

// Create missing views
await _dbValidation.CreateMissingDatabaseViewsAsync();

// Validate existing views
await _dbValidation.ValidateAndUpdateExistingViewsAsync();

// Check specific view
if (!await _dbValidation.ViewExistsSysproAsync("CG_InventoryOrdering_View"))
{
throw new ApplicationException("Required view missing");
}
}
}

Example 3: Resource Loading

// Loading and using embedded resources
public class ReportGenerator
{
public string GenerateReport(ReportData data)
{
// Load report template
var template = ResourceHelper.GetResourceFileText("Reports.OrderTemplate.html");

// Load CSS styles
var styles = ResourceHelper.GetResourceFileText("Reports.Styles.css");

// Replace placeholders
template = template.Replace("{{STYLES}}", styles);
template = template.Replace("{{ORDER_NUMBER}}", data.OrderNumber);
template = template.Replace("{{ORDER_DATE}}", data.OrderDate.ToString());

// Load logo as base64
var logoBytes = ResourceHelper.GetResourceFileBytes("Images.Logo.png");
var logoBase64 = Convert.ToBase64String(logoBytes);
template = template.Replace("{{LOGO}}", $"data:image/png;base64,{logoBase64}");

return template;
}
}

Best Practices

Configuration Management

  • Use appropriate scopes (Program, Company, Operator)
  • Provide sensible defaults
  • Validate configuration values
  • Cache frequently accessed settings

Resource Management

  • Embed resources that don't change frequently
  • Use meaningful resource names
  • Organize resources in folders
  • Handle missing resources gracefully

Database Validation

  • Run validation during startup
  • Log all schema changes
  • Provide rollback scripts
  • Test view creation in development

Performance Optimization

  • Implement pagination for large datasets
  • Cache factory-created objects when appropriate
  • Use async methods for I/O operations
  • Dispose resources properly

Common Pitfalls

Resource Not Found

// BAD: Hardcoded resource path
var text = File.ReadAllText(@"C:\Resources\template.txt");

// GOOD: Embedded resource
var text = ResourceHelper.GetResourceFileText("Templates.template.txt");

Settings Scope Confusion

// BAD: Always using program scope
await _settings.SetByProgramAsync("UserPreference", value);

// GOOD: Use appropriate scope
if (IsUserSpecific)
await _settings.SetByOperatorAsync("UserPreference", value);
else
await _settings.SetByCompanyAsync("CompanyDefault", value);

Missing Validation

// BAD: Assuming database objects exist
var data = context.CG_CustomView.ToList();

// GOOD: Validate first
if (await _dbValidation.ViewExistsSysproAsync("CG_CustomView"))
{
var data = context.CG_CustomView.ToList();
}

Summary

The utility services layer provides essential supporting functionality that enables the smooth operation of the MiniMrpOrderCreation dashboard. Through comprehensive configuration management, database validation, resource handling, and helper services, this layer ensures that the application has a solid foundation of reusable, well-tested utilities. The services handle cross-cutting concerns efficiently while maintaining separation of concerns and promoting code reuse across the application.