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}")]
@using FluentStorage;
@using FluentStorage.Storage;
@using System.Text.Json;
@using Microsoft.Extensions.Options;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Models.Configuration;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.StayExport;
@inject IStayExportFileEntityService stayExportEntityService
@inject NavigationManager Navigation
@@ -15,7 +12,7 @@
@inject IDataListService dataListService;
@inject ILocalizerService Localizer;
@inject IOptions<StayExportConfig> exportConfig
@inject IStayExportService ExportService;
@inject IJSRuntime JS
@@ -138,10 +135,14 @@
return;
}
IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString);
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);
using var streamRef = new DotNetStreamReference(stream: objectStream);
@@ -1,11 +1,7 @@
@attribute [Route($"{GuestConsts.Page_StayExport_New}")]
@using FluentValidation;
@using FluentStorage;
@using FluentStorage.Storage;
@using Microsoft.Extensions.Options;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Models.Configuration;
@using Somnia.GuestLog.Enums.Models;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators;
@@ -17,9 +13,8 @@
@inject NavigationManager Navigation
@inject IValidator<StayExportParamsDTO> ExportParamsValidator;
@inject ILocalizerService Localizer;
@inject IStayExportServiceFactory ExportServiceFactory;
@inject IStayExportService ExportService;
@inject IOptions<StayExportConfig> exportConfig
@inject IJSRuntime JS
@@ -121,9 +116,7 @@
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
var exportService = ExportServiceFactory.GetService(exportFormat.Value);
var exportResult = await exportService.ExportStaysByParams(exportParams);
var exportResult = await ExportService.ExportStaysByParamsAsync(exportFormat.Value, exportParams);
exportedFile = exportResult.Value;
loadingDialog.Close();
@@ -141,7 +134,6 @@
private async Task BtnDownloadExportedFile()
{
Debug.Assert(exportedFile != null);
Debug.Assert(exportConfig.Value.ExportStorageConnectionString != null);
if (string.IsNullOrEmpty(exportedFile.ExportedPath))
{
@@ -149,16 +141,19 @@
return;
}
IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString);
using Stream dataStream = new MemoryStream();
using Stream objectStream = new MemoryStream();
await store.GetObject(exportedFile.ExportedPath, objectStream);
objectStream.Seek(0, SeekOrigin.Begin);
Result loadResult = await ExportService.GetExportedFileAsync(exportedFile, dataStream);
if (!loadResult.IsSuccess)
{
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);
using var streamRef = new DotNetStreamReference(stream: objectStream);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
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.AddScoped<IUserMessageService, UserMessageService>();
builder.Services.AddSingleton<ILocalizerService, LocalizerService>();
builder.Services.AddSingleton<IStayExportService, StayExportService>();
// Add Factories (and it's services)
builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>();
builder.Services.AddFactoryDI<IStayExportServiceFactory, ConfigFileStaytExportServiceFactory>();
builder.Services.AddFactoryDI<IStayExportFormatProviderFactory, StayExportFormatProviderFactory>();
// Add Options config
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;
public interface IStayExportServiceFactory
public interface IStayExportFormatProviderFactory
{
/// <summary>
/// Returns the StayExport service implementation given current configuration.
/// </summary>
public IStayExportService GetService(StayExportFormat exportFormat);
public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat);
}
@@ -5,7 +5,10 @@ namespace Somnia.GuestLog.Services.StayExport;
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;
public class ConfigFileStaytExportServiceFactory(
public class StayExportFormatProviderFactory(
IServiceProvider serviceProvider,
IOptions<StayExportConfig> config
) : IStayExportServiceFactory, IFactoryDI
) : IStayExportFormatProviderFactory, IFactoryDI
{
public static void RegisterFactoryItems(IServiceCollection services)
{
services.AddKeyedSingleton<IStayExportService, JsonStayExportService>(StayExportFormat.GuestLogJson);
services.AddKeyedSingleton<IStayExportService, SpainParteViajerosStayExportService>(StayExportFormat.SpainParteViajeros);
services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderJson>(StayExportFormat.GuestLogJson);
services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderSpainParteViajeros>(StayExportFormat.SpainParteViajeros);
}
public IStayExportService GetService(StayExportFormat exportFormat)
public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat)
{
if (config.Value.EnableStayExport == false)
{
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}'");
}
}
@@ -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.Storage;
using Microsoft.Extensions.Options;
@@ -11,23 +11,21 @@ using Somnia.GuestLog.Services.Entity;
namespace Somnia.GuestLog.Services.StayExport;
public class JsonStayExportService(
ILogger<JsonStayExportService> logger,
public class StayExportService(
ILogger<StayExportProviderJson> logger,
IOptions<StayExportConfig> exportConfig,
IStayExportFileEntityService stayExportEntityService,
IStayEntryEntityService stayEntityService,
IOptions<StayExportConfig> exportConfig
IStayExportFormatProviderFactory providerFactory
) : IStayExportService
{
private const string fileExtension = "json";
private static readonly StayEntryQueryConfig queryConfig =
new(IncludeGuests: true, UseSplitQueries: true, DisableTracking: true);
private readonly StayExportConfig config = exportConfig.Value;
public StayExportFormat GetFormat() => StayExportFormat.GuestLogJson;
public async Task<Result<StayExportFileDTO>> ExportStaysByParams(StayExportParamsDTO exportParams)
public async Task<Result<StayExportFileDTO>> ExportStaysByParamsAsync(StayExportFormat format, StayExportParamsDTO exportParams)
{
//Get data to export.
IEnumerable<StayEntryDTO>? stayList;
@@ -45,7 +43,7 @@ public class JsonStayExportService(
DateTime exportTime = DateTime.UtcNow;
StayExportFileDTO stayExport = new()
{
ExportFormat = GetFormat(),
ExportFormat = format,
ExportParams = exportParams,
ExportTime = exportTime,
};
@@ -54,13 +52,18 @@ public class JsonStayExportService(
using Stream exportedDataStream = new MemoryStream();
//Generate data to export.
StayExportFileResult? exportResult;
StayExportFileResult exportResult;
string? exportFilename = null;
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.
exportedDataStream.Seek(0, SeekOrigin.Begin);
exportFilename = provider.GetFilename(exportTime);
}
catch (Exception ex)
{
@@ -73,7 +76,14 @@ public class JsonStayExportService(
{
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.
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
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}");
}
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()
{
string? storageConnString = config.ExportStorageConnectionString;