feat StayExport - Refactor Export Services.

- Created new service: IStayExportService" for common export operations.
- Updated older services to be "IStayExportFormatProvider". This services will only implement the export format.
- Renamed "IStayExportServiceFactory" to "IStayExportFormatProviderFactory" as this factory now is used to get the format providers for the given format.
This commit is contained in:
2026-09-04 22:33:01 +02:00
parent 058352b7f3
commit 6fe5a5e5a3
11 changed files with 157 additions and 91 deletions
@@ -1,12 +1,9 @@
@attribute [Route($"{GuestConsts.Page_StayExport_List}")] @attribute [Route($"{GuestConsts.Page_StayExport_List}")]
@using FluentStorage;
@using FluentStorage.Storage;
@using System.Text.Json; @using System.Text.Json;
@using Microsoft.Extensions.Options;
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Models.Configuration;
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.StayExport;
@inject IStayExportFileEntityService stayExportEntityService @inject IStayExportFileEntityService stayExportEntityService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@@ -15,7 +12,7 @@
@inject IDataListService dataListService; @inject IDataListService dataListService;
@inject ILocalizerService Localizer; @inject ILocalizerService Localizer;
@inject IOptions<StayExportConfig> exportConfig @inject IStayExportService ExportService;
@inject IJSRuntime JS @inject IJSRuntime JS
@@ -138,10 +135,14 @@
return; return;
} }
IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString);
using Stream objectStream = new MemoryStream(); using Stream objectStream = new MemoryStream();
await store.GetObject(item.ExportedPath, objectStream); Result loadResult = await ExportService.GetExportedFileAsync(item, objectStream);
if (!loadResult.IsSuccess)
{
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
objectStream.Seek(0, SeekOrigin.Begin); objectStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: objectStream); using var streamRef = new DotNetStreamReference(stream: objectStream);
@@ -1,11 +1,7 @@
@attribute [Route($"{GuestConsts.Page_StayExport_New}")] @attribute [Route($"{GuestConsts.Page_StayExport_New}")]
@using FluentValidation; @using FluentValidation;
@using FluentStorage;
@using FluentStorage.Storage;
@using Microsoft.Extensions.Options;
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Models.Configuration;
@using Somnia.GuestLog.Enums.Models; @using Somnia.GuestLog.Enums.Models;
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators; @using Somnia.GuestLog.Validators;
@@ -17,9 +13,8 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IValidator<StayExportParamsDTO> ExportParamsValidator; @inject IValidator<StayExportParamsDTO> ExportParamsValidator;
@inject ILocalizerService Localizer; @inject ILocalizerService Localizer;
@inject IStayExportServiceFactory ExportServiceFactory; @inject IStayExportService ExportService;
@inject IOptions<StayExportConfig> exportConfig
@inject IJSRuntime JS @inject IJSRuntime JS
@@ -121,9 +116,7 @@
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>(); var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
var exportService = ExportServiceFactory.GetService(exportFormat.Value); var exportResult = await ExportService.ExportStaysByParamsAsync(exportFormat.Value, exportParams);
var exportResult = await exportService.ExportStaysByParams(exportParams);
exportedFile = exportResult.Value; exportedFile = exportResult.Value;
loadingDialog.Close(); loadingDialog.Close();
@@ -141,7 +134,6 @@
private async Task BtnDownloadExportedFile() private async Task BtnDownloadExportedFile()
{ {
Debug.Assert(exportedFile != null); Debug.Assert(exportedFile != null);
Debug.Assert(exportConfig.Value.ExportStorageConnectionString != null);
if (string.IsNullOrEmpty(exportedFile.ExportedPath)) if (string.IsNullOrEmpty(exportedFile.ExportedPath))
{ {
@@ -149,16 +141,19 @@
return; return;
} }
IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString); using Stream dataStream = new MemoryStream();
using Stream objectStream = new MemoryStream(); Result loadResult = await ExportService.GetExportedFileAsync(exportedFile, dataStream);
await store.GetObject(exportedFile.ExportedPath, objectStream); if (!loadResult.IsSuccess)
objectStream.Seek(0, SeekOrigin.Begin); {
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
dataStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: dataStream);
var fileName = Path.GetFileName(exportedFile.ExportedPath); var fileName = Path.GetFileName(exportedFile.ExportedPath);
using var streamRef = new DotNetStreamReference(stream: objectStream);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef); await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success); userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success);
+2 -1
View File
@@ -66,10 +66,11 @@ builder.Services.AddSingleton<IStayExportFileEntityService, StayExportFileEntity
builder.Services.AddSingleton<IDataListService, StaticDataListService>(); builder.Services.AddSingleton<IDataListService, StaticDataListService>();
builder.Services.AddScoped<IUserMessageService, UserMessageService>(); builder.Services.AddScoped<IUserMessageService, UserMessageService>();
builder.Services.AddSingleton<ILocalizerService, LocalizerService>(); builder.Services.AddSingleton<ILocalizerService, LocalizerService>();
builder.Services.AddSingleton<IStayExportService, StayExportService>();
// Add Factories (and it's services) // Add Factories (and it's services)
builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>(); builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>();
builder.Services.AddFactoryDI<IStayExportServiceFactory, ConfigFileStaytExportServiceFactory>(); builder.Services.AddFactoryDI<IStayExportFormatProviderFactory, StayExportFormatProviderFactory>();
// Add Options config // Add Options config
builder.Services.AddOptions<AddressQueryConfig>() builder.Services.AddOptions<AddressQueryConfig>()
@@ -0,0 +1,19 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportFormatProvider
{
public StayExportFormat GetFormat();
public string GetFilename(DateTime exportTime);
/// <summary>
/// Export Data with the corresponding format.
/// Data is copied to given stream (no other operations are performed to the stream).
/// </summary>
/// <param name="stayList">List of Stays to export</param>
/// <param name="dataStream">Stream to copy exported data.</param>
public Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream);
}
@@ -2,10 +2,10 @@ using Somnia.GuestLog.Enums.Models;
namespace Somnia.GuestLog.Services.StayExport; namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportServiceFactory public interface IStayExportFormatProviderFactory
{ {
/// <summary> /// <summary>
/// Returns the StayExport service implementation given current configuration. /// Returns the StayExport service implementation given current configuration.
/// </summary> /// </summary>
public IStayExportService GetService(StayExportFormat exportFormat); public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat);
} }
@@ -5,7 +5,10 @@ namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportService public interface IStayExportService
{ {
public StayExportFormat GetFormat(); public Task<Result<StayExportFileDTO>> ExportStaysByParamsAsync(StayExportFormat format, StayExportParamsDTO exportParams);
public Task<Result<StayExportFileDTO>> ExportStaysByParams(StayExportParamsDTO exportParams);
public Task<Result> GetExportedFileAsync(StayExportFileDTO item, Stream dataStream);
public Task<Result> DeleteExportedFileAsync(StayExportFileDTO item);
} }
@@ -1,17 +0,0 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public class SpainParteViajerosStayExportService(
) : IStayExportService
{
public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros;
public Task<Result<StayExportFileDTO>> ExportStaysByParams(StayExportParamsDTO exportParams)
{
throw new NotImplementedException();
}
}
@@ -5,26 +5,26 @@ using Somnia.GuestLog.Utils.DI;
namespace Somnia.GuestLog.Services.StayExport; namespace Somnia.GuestLog.Services.StayExport;
public class ConfigFileStaytExportServiceFactory( public class StayExportFormatProviderFactory(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IOptions<StayExportConfig> config IOptions<StayExportConfig> config
) : IStayExportServiceFactory, IFactoryDI ) : IStayExportFormatProviderFactory, IFactoryDI
{ {
public static void RegisterFactoryItems(IServiceCollection services) public static void RegisterFactoryItems(IServiceCollection services)
{ {
services.AddKeyedSingleton<IStayExportService, JsonStayExportService>(StayExportFormat.GuestLogJson); services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderJson>(StayExportFormat.GuestLogJson);
services.AddKeyedSingleton<IStayExportService, SpainParteViajerosStayExportService>(StayExportFormat.SpainParteViajeros); services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderSpainParteViajeros>(StayExportFormat.SpainParteViajeros);
} }
public IStayExportService GetService(StayExportFormat exportFormat) public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat)
{ {
if (config.Value.EnableStayExport == false) if (config.Value.EnableStayExport == false)
{ {
throw new InvalidOperationException("StayExport is disabled. Enable and configure it first!"); throw new InvalidOperationException("StayExport is disabled. Enable and configure it first!");
} }
return serviceProvider.GetKeyedService<IStayExportService>(exportFormat) return serviceProvider.GetKeyedService<IStayExportFormatProvider>(exportFormat)
?? throw new InvalidOperationException($"StayExport service not found for ExportFormat: '{exportFormat}'"); ?? throw new InvalidOperationException($"StayExport service not found for ExportFormat: '{exportFormat}'");
} }
} }
@@ -0,0 +1,27 @@
using System.Text.Json;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportProviderJson : IStayExportFormatProvider
{
private const string fileExtension = "json";
public StayExportFormat GetFormat() => StayExportFormat.GuestLogJson;
public string GetFilename(DateTime exportTime)
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public async Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{
if (!stayList.Any())
{
return StayExportFileResult.NoData;
}
// Convert to JSON.
await JsonSerializer.SerializeAsync(dataStream, stayList);
return StayExportFileResult.OK;
}
}
@@ -0,0 +1,20 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportProviderSpainParteViajeros(
) : IStayExportFormatProvider
{
private const string fileExtension = "xml";
public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros;
public string GetFilename(DateTime exportTime)
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{
throw new NotImplementedException();
}
}
@@ -1,4 +1,4 @@
using System.Text.Json; using System.Diagnostics;
using FluentStorage; using FluentStorage;
using FluentStorage.Storage; using FluentStorage.Storage;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -11,23 +11,21 @@ using Somnia.GuestLog.Services.Entity;
namespace Somnia.GuestLog.Services.StayExport; namespace Somnia.GuestLog.Services.StayExport;
public class JsonStayExportService( public class StayExportService(
ILogger<JsonStayExportService> logger, ILogger<StayExportProviderJson> logger,
IOptions<StayExportConfig> exportConfig,
IStayExportFileEntityService stayExportEntityService, IStayExportFileEntityService stayExportEntityService,
IStayEntryEntityService stayEntityService, IStayEntryEntityService stayEntityService,
IOptions<StayExportConfig> exportConfig IStayExportFormatProviderFactory providerFactory
) : IStayExportService ) : IStayExportService
{ {
private const string fileExtension = "json";
private static readonly StayEntryQueryConfig queryConfig = private static readonly StayEntryQueryConfig queryConfig =
new(IncludeGuests: true, UseSplitQueries: true, DisableTracking: true); new(IncludeGuests: true, UseSplitQueries: true, DisableTracking: true);
private readonly StayExportConfig config = exportConfig.Value; private readonly StayExportConfig config = exportConfig.Value;
public StayExportFormat GetFormat() => StayExportFormat.GuestLogJson;
public async Task<Result<StayExportFileDTO>> ExportStaysByParamsAsync(StayExportFormat format, StayExportParamsDTO exportParams)
public async Task<Result<StayExportFileDTO>> ExportStaysByParams(StayExportParamsDTO exportParams)
{ {
//Get data to export. //Get data to export.
IEnumerable<StayEntryDTO>? stayList; IEnumerable<StayEntryDTO>? stayList;
@@ -45,7 +43,7 @@ public class JsonStayExportService(
DateTime exportTime = DateTime.UtcNow; DateTime exportTime = DateTime.UtcNow;
StayExportFileDTO stayExport = new() StayExportFileDTO stayExport = new()
{ {
ExportFormat = GetFormat(), ExportFormat = format,
ExportParams = exportParams, ExportParams = exportParams,
ExportTime = exportTime, ExportTime = exportTime,
}; };
@@ -54,13 +52,18 @@ public class JsonStayExportService(
using Stream exportedDataStream = new MemoryStream(); using Stream exportedDataStream = new MemoryStream();
//Generate data to export. //Generate data to export.
StayExportFileResult? exportResult; StayExportFileResult exportResult;
string? exportFilename = null;
try try
{ {
exportResult = await ExportDataToStream(stayList, exportedDataStream); //Create Format provider and export data to stream.
var provider = providerFactory.GetProvider(format);
exportResult = await provider.ExportDataAsync(stayList, exportedDataStream);
// "Rewind" stream so it can be read from the begining when uploading. // "Rewind" stream so it can be read from the begining when uploading.
exportedDataStream.Seek(0, SeekOrigin.Begin); exportedDataStream.Seek(0, SeekOrigin.Begin);
exportFilename = provider.GetFilename(exportTime);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -73,7 +76,14 @@ public class JsonStayExportService(
{ {
try try
{ {
string exportPath = GetExportPath(exportTime); Debug.Assert(exportFilename != null);
string exportPathSubdir = "";
if (config.UseSubdirectoryByFormat)
{
exportPathSubdir = Enum.GetName(format) ?? "";
}
string exportPath = Path.Combine(exportPathSubdir, exportFilename);
//Store data. //Store data.
using IStore exportStore = GetExportStore(); using IStore exportStore = GetExportStore();
@@ -128,6 +138,38 @@ public class JsonStayExportService(
} }
public async Task<Result> GetExportedFileAsync(StayExportFileDTO item, Stream dataStream)
{
if (string.IsNullOrEmpty(item.ExportedPath))
{
logger.LogWarning("Trying to get exported file without path!! Exported file can't be loaded!");
return Result.Fail(ResultCode.NotFound);
}
try
{
IStore store = GetExportStore();
await store.GetObject(item.ExportedPath, dataStream);
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting exported file '{id}' from path '{path}'", item.Id, item.ExportedPath);
return Result.FailUnknownError();
}
return Result.Ok();
}
public Task<Result> DeleteExportedFileAsync(StayExportFileDTO item)
{
throw new NotImplementedException();
//TODO:
// - Delete file from "Store" if exists.
// - Delete entry from DB.
}
#region Internal Methods #region Internal Methods
private async Task<IEnumerable<StayEntryDTO>> GetStaysToExportByParamsAsync(StayExportParamsDTO exportParams) private async Task<IEnumerable<StayEntryDTO>> GetStaysToExportByParamsAsync(StayExportParamsDTO exportParams)
@@ -152,31 +194,6 @@ public class JsonStayExportService(
throw new InvalidOperationException($"StayExportFile - Error getting stays using params: export type invalid: {exportParams.ExportType}"); throw new InvalidOperationException($"StayExportFile - Error getting stays using params: export type invalid: {exportParams.ExportType}");
} }
private async Task<StayExportFileResult> ExportDataToStream(IEnumerable<StayEntryDTO> stayList, Stream exportStream)
{
if (!stayList.Any())
{
return StayExportFileResult.NoData;
}
// Convert to JSON.
await JsonSerializer.SerializeAsync(exportStream, stayList);
return StayExportFileResult.OK;
}
private string GetExportPath(DateTime time)
{
string formatName = GetFormatName();
string exportPathSubdir = config.UseSubdirectoryByFormat ? formatName : "";
string exportFileName = $"{formatName}_{time.ToFileTimeUtc()}.{fileExtension}";
return Path.Combine(exportPathSubdir, exportFileName);
}
private string GetFormatName()
{
return Enum.GetName(GetFormat()) ?? throw new InvalidOperationException("Can't get StayExportFormat name!");
}
private IStore GetExportStore() private IStore GetExportStore()
{ {
string? storageConnString = config.ExportStorageConnectionString; string? storageConnString = config.ExportStorageConnectionString;