Add unit tests for Fuchs_DataService and related components
- Introduced comprehensive unit tests for the Fuchs_DataService library, covering DATEV header formatting, CSV/XML generation, and FdsMfrClient construction. - Implemented tests for FdsMfr.UpdateNeed parsing and FdsShared utility helpers, ensuring correct functionality and stability. - Added tests for FdsConfig and FdsMfrClient to validate configuration resolution and client construction. Document decisions on backend-authoritative invoice and reminder handling - Created ADR 0008 to clarify that all invoice types and reminder stages are backend-authoritative during drafting and previewing. - Established that all calculations and settings must be processed server-side, ensuring consistency between online editor and PDF outputs. Define irreversible mutations for set-price modes in invoices - Documented ADR 0009 to specify that the "Set mit Preis" and "Nur Set mit Preis" operations are irreversible mutations affecting service request blocks. - Clarified that these operations are not display toggles but actual data changes, ensuring clear expectations for invoice handling. Transition MFR ERP sync to in-process execution within the web app - Created ADR 0010 to outline the migration of Fuchs_DataService from a standalone service to an in-process library within the Fuchs web application. - Updated configuration and logging management to be handled by the host application, streamlining the sync process. Add publish profile and periodic hosted service for job scheduling - Introduced a publish profile for deployment to a specified folder. - Implemented PeriodicHostedService to manage multiple independent jobs, including the MFR ERP sync, with configurable execution intervals. Add dotnet-tools.json for EF Core CLI tools - Included dotnet-tools.json to manage the version of dotnet-ef for Entity Framework Core migrations and commands.
This commit is contained in:
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
using fds.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Topshelf;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class FdsService : ServiceControl
|
||||
{
|
||||
private readonly PeriodicHostedService _hostedService;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public FdsService()
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(b =>
|
||||
b.SetMinimumLevel(LogLevel.Debug).AddFdsLogging());
|
||||
|
||||
var mfr = new FdsMfr(
|
||||
loggerFactory.CreateLogger<FdsMfr>(),
|
||||
loggerFactory);
|
||||
var interval = TimeSpan.FromMinutes(FdsConfig.ExecutionFrequency_Minutes);
|
||||
var jobs = new[]
|
||||
{
|
||||
new PeriodicJobDefinition("MfrSync", interval, async ct =>
|
||||
{
|
||||
bool debug = FdsConfig.DebugDetails;
|
||||
await mfr.UpdateIfNecessary_async(debug, ct);
|
||||
await mfr.UpdateRequested_async(debug, ct);
|
||||
await mfr.GetInvoiceFiles_async(debug, ct);
|
||||
})
|
||||
};
|
||||
|
||||
var logger = loggerFactory.CreateLogger<PeriodicHostedService>();
|
||||
_hostedService = new PeriodicHostedService(jobs, logger);
|
||||
}
|
||||
|
||||
public bool Start(HostControl hostControl)
|
||||
{
|
||||
_ = _hostedService.StartAsync(_cts.Token);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Stop(HostControl hostControl)
|
||||
{
|
||||
_cts.Cancel();
|
||||
_hostedService.StopAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FdsMainModule
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
FdsConfig.Initialize();
|
||||
|
||||
string machineName = Environment.MachineName.ToLower();
|
||||
if (!new[] { "digital-pc", "digital-dpc" }.Contains(machineName))
|
||||
{
|
||||
HostFactory.Run(x =>
|
||||
{
|
||||
x.Service<FdsService>(s =>
|
||||
{
|
||||
s.ConstructUsing(name => new FdsService());
|
||||
s.WhenStarted((tc, host) => tc.Start(host));
|
||||
s.WhenStopped((tc, host) => tc.Stop(host));
|
||||
s.BeforeStoppingService(ctx =>
|
||||
{
|
||||
if (FdsConfig.DebugDetails) Task.Run(() => FdsDebug.DebugToFile("fds__data_service - beforestop", filename: "DebugDetail.txt"));
|
||||
});
|
||||
s.WhenPaused((tc, host) => tc.Stop(host));
|
||||
s.WhenContinued((tc, host) => tc.Start(host));
|
||||
});
|
||||
x.EnablePauseAndContinue();
|
||||
x.StartAutomatically();
|
||||
x.RunAsLocalSystem();
|
||||
x.SetDescription("MFR Data Sync");
|
||||
x.SetDisplayName("MFR Data Sync");
|
||||
x.SetServiceName("MFR Data Sync");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var svc = new FdsService();
|
||||
svc.Start(null!);
|
||||
Console.WriteLine("Running locally — press any key to stop.");
|
||||
Console.ReadKey();
|
||||
svc.Stop(null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,16 +319,13 @@ public class FdsMfr : IFdsMfr
|
||||
var archiveFile = new FileInfo(Path.GetTempPath() + $"DatevUpload_AR {startdate:yyyyMM}_{admin["mode"]}.zip");
|
||||
try
|
||||
{
|
||||
using var archive = new Archive(archiveFile, type: SevenZip.OutArchiveFormat.Zip,
|
||||
logger: _loggerFactory.CreateLogger<Archive>());
|
||||
if (archive.CompressToStream(fls, targetstream: stream!))
|
||||
{
|
||||
stream!.Position = 0;
|
||||
return archiveFile;
|
||||
}
|
||||
_logger.LogError(
|
||||
"getDatevZip: CompressToStream returned false — archiveFile={ArchiveFile} fileCount={FileCount}",
|
||||
archiveFile.FullName, fls.Count);
|
||||
// Native .NET zip via the OCORE helper (System.IO.Compression) — no external
|
||||
// 7-Zip dependency. The DATEV export is a plain, unencrypted zip, so ZipArchive
|
||||
// covers it fully. filesToZipArchive skips null/empty entries.
|
||||
var zipBytes = Task.Run(async () => await OCORE.zip.filesToZipArchive(fls)).Result;
|
||||
stream!.Write(zipBytes, 0, zipBytes.Length);
|
||||
stream.Position = 0;
|
||||
return archiveFile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -2,29 +2,22 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace fds;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the application <see cref="IConfiguration"/> built from appsettings.json.
|
||||
/// Call <see cref="Initialize()"/> once at startup before accessing <see cref="Current"/>.
|
||||
/// Holds the application <see cref="IConfiguration"/> supplied by the host (the Fuchs web app).
|
||||
/// Fuchs_DataService is a library — it has no configuration of its own; the host owns the
|
||||
/// connection strings and MFR credentials and injects them via <see cref="Initialize"/>,
|
||||
/// which <c>Program.cs</c> calls once at startup before any <see cref="FdsMfr"/> use.
|
||||
/// </summary>
|
||||
public static class FdsConfig
|
||||
{
|
||||
private static IConfiguration? _config;
|
||||
|
||||
internal static IConfiguration Current =>
|
||||
_config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize() in Main().");
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
_config = new ConfigurationBuilder()
|
||||
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
|
||||
.Build();
|
||||
}
|
||||
_config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize(configuration) at host startup.");
|
||||
|
||||
public static void Initialize(IConfiguration configuration)
|
||||
{
|
||||
@@ -32,21 +25,11 @@ public static class FdsConfig
|
||||
}
|
||||
|
||||
// -- Connection strings ---------------------------------------------------
|
||||
internal static string SQLConnectionString() =>
|
||||
Current.GetConnectionString("fuchs_ConnectionString")
|
||||
?? throw new InvalidOperationException("Missing connection string: fuchs_ConnectionString");
|
||||
|
||||
internal static string FDSConnectionString() =>
|
||||
Current.GetConnectionString("fuchs_fds_ConnectionString")
|
||||
?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString");
|
||||
|
||||
// -- App settings ---------------------------------------------------------
|
||||
internal static double ExecutionFrequency_Minutes =>
|
||||
Current.GetValue<double>("Fds:ExecutionFrequency_Minutes", 15);
|
||||
|
||||
internal static bool DebugDetails =>
|
||||
Current.GetValue<bool>("Fds:DebugDetails");
|
||||
|
||||
// -- MFR credentials (supplied by the host from Key Vault / appsettings) --
|
||||
internal static string MFR_UserName =>
|
||||
Current["Fds:MFR_UserName"] ?? "";
|
||||
|
||||
@@ -59,12 +42,8 @@ public static class FdsConfig
|
||||
|
||||
internal static class FdsShared
|
||||
{
|
||||
internal static string SQLConnectionString() => FdsConfig.SQLConnectionString();
|
||||
internal static string FDSConnectionString() => FdsConfig.FDSConnectionString();
|
||||
|
||||
internal static SqlConnection SqlCon() =>
|
||||
new(FdsConfig.SQLConnectionString());
|
||||
|
||||
public static string RandomString(byte length)
|
||||
{
|
||||
var r = new Random();
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SevenZip;
|
||||
|
||||
namespace fds;
|
||||
|
||||
public class Archive : IDisposable
|
||||
{
|
||||
public event Action? FileSaved;
|
||||
public event Action? FileStreamCreated;
|
||||
|
||||
private FileInfo _archiveFile;
|
||||
private string _archivePassword;
|
||||
private OutArchiveFormat _archiveFormat;
|
||||
private readonly ILogger<Archive> _logger;
|
||||
public string TempPath { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
|
||||
public Stream? ArchiveFileStream { get; set; }
|
||||
|
||||
private SevenZipExtractor? _zipOut;
|
||||
private SevenZipCompressor? _zipIn;
|
||||
public bool ZipAppend { get; set; } = true;
|
||||
public bool ExitOK { get; set; }
|
||||
public bool ZipInOK { get; set; }
|
||||
|
||||
public Archive(FileInfo archiveFile, string archivePassword = "", bool init = true,
|
||||
OutArchiveFormat type = OutArchiveFormat.SevenZip, ILogger<Archive>? logger = null)
|
||||
{
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<Archive>.Instance;
|
||||
_archiveFormat = type;
|
||||
_archiveFile = new FileInfo(archiveFile.FullName.Replace(archiveFile.Extension,
|
||||
type == OutArchiveFormat.SevenZip ? ".7z" : archiveFile.Extension));
|
||||
_archivePassword = archivePassword;
|
||||
if (init) InitZipIn(type);
|
||||
}
|
||||
|
||||
private void InitZipIn(OutArchiveFormat type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyDir = new DirectoryInfo(
|
||||
new Uri(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!).LocalPath);
|
||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
{
|
||||
var assemblyDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
||||
_logger.LogError("SevenZipPath not found — 7z.dll is missing from the output directory.");
|
||||
}
|
||||
SevenZipCompressor.SetLibraryPath(Zipping.SevenZipPath);
|
||||
|
||||
_zipIn = new SevenZipCompressor
|
||||
{
|
||||
ArchiveFormat = (type == OutArchiveFormat.SevenZip && _archiveFile.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase))
|
||||
? OutArchiveFormat.SevenZip : type,
|
||||
CompressionLevel = SevenZip.CompressionLevel.Ultra,
|
||||
CompressionMode = ZipAppend ? SevenZip.CompressionMode.Append : SevenZip.CompressionMode.Create,
|
||||
DirectoryStructure = false
|
||||
};
|
||||
|
||||
_zipIn.CompressionMethod = _zipIn.ArchiveFormat switch
|
||||
{
|
||||
OutArchiveFormat.SevenZip => CompressionMethod.Lzma2,
|
||||
OutArchiveFormat.Zip or OutArchiveFormat.GZip => CompressionMethod.Deflate,
|
||||
_ => CompressionMethod.Default
|
||||
};
|
||||
|
||||
ZipInOK = true;
|
||||
}
|
||||
|
||||
public void Extract(FileInfo dataArchiveFilePath, DirectoryInfo tgtDirectory, OutArchiveFormat type = default)
|
||||
{
|
||||
if (!dataArchiveFilePath.Exists) return;
|
||||
|
||||
if (type == default)
|
||||
type = dataArchiveFilePath.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase)
|
||||
? OutArchiveFormat.SevenZip : OutArchiveFormat.Zip;
|
||||
|
||||
if (!ZipInOK) InitZipIn(type);
|
||||
|
||||
_zipOut = string.IsNullOrEmpty(_archivePassword)
|
||||
? new SevenZipExtractor(dataArchiveFilePath.FullName)
|
||||
: new SevenZipExtractor(dataArchiveFilePath.FullName, _archivePassword);
|
||||
|
||||
try
|
||||
{
|
||||
if (!_zipOut.ArchiveFileData[0].Encrypted && !string.IsNullOrEmpty(_archivePassword))
|
||||
_archivePassword = "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Archive.Extract failed — path={Path}, target={Target}",
|
||||
dataArchiveFilePath.FullName, tgtDirectory.FullName);
|
||||
return;
|
||||
}
|
||||
|
||||
_zipOut.ExtractArchive(tgtDirectory.FullName);
|
||||
_zipOut.Dispose();
|
||||
}
|
||||
|
||||
public bool Compress(List<FileInfo> files, FileInfo? archiveFile = null, string? archivePass = null, OutArchiveFormat type = OutArchiveFormat.SevenZip)
|
||||
{
|
||||
if (files.Count == 0) return true;
|
||||
if (!ZipInOK) InitZipIn(type);
|
||||
|
||||
archiveFile ??= _archiveFile;
|
||||
archivePass = string.IsNullOrEmpty(archivePass) ? _archivePassword : archivePass;
|
||||
|
||||
if (archiveFile.Exists && ZipAppend)
|
||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
||||
else
|
||||
{
|
||||
if (archiveFile.Exists) archiveFile.Delete();
|
||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filesVerified = files.Where(f => f.Exists).ToArray();
|
||||
var filePaths = filesVerified.Select(f => f.FullName).ToArray();
|
||||
if (string.IsNullOrEmpty(archivePass))
|
||||
_zipIn.CompressFiles(archiveFile.FullName, filePaths);
|
||||
else
|
||||
{
|
||||
_zipIn.EncryptHeaders = true;
|
||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
||||
_zipIn.CompressFilesEncrypted(archiveFile.FullName, archivePass, filePaths);
|
||||
}
|
||||
FileSaved?.Invoke();
|
||||
ExitOK = true;
|
||||
_zipIn = null;
|
||||
ZipInOK = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Previously silent: callers (e.g. HandleDatevZip) saw only a bare failed result
|
||||
// with no way to tell a compression error from any other reason ExitOK is false.
|
||||
_logger.LogError(ex, "Archive.Compress failed — archiveFile={ArchiveFile} fileCount={FileCount}",
|
||||
archiveFile.FullName, files.Count);
|
||||
ExitOK = false;
|
||||
}
|
||||
archiveFile.Refresh();
|
||||
return ExitOK && archiveFile.Exists;
|
||||
}
|
||||
|
||||
public bool CompressToStream(Dictionary<string, byte[]> files, Stream? targetstream = null)
|
||||
{
|
||||
if (files.Count == 0) return true;
|
||||
if (!ZipInOK) InitZipIn(_archiveFormat);
|
||||
|
||||
if (ArchiveFileStream == null)
|
||||
{
|
||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
||||
ArchiveFileStream = new MemoryStream();
|
||||
}
|
||||
else
|
||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
||||
|
||||
try
|
||||
{
|
||||
var filesStreams = files.ToDictionary(kv => kv.Key, kv => (Stream)new MemoryStream(kv.Value));
|
||||
var target = targetstream ?? ArchiveFileStream;
|
||||
if (string.IsNullOrEmpty(_archivePassword))
|
||||
_zipIn.CompressStreamDictionary(filesStreams, target);
|
||||
else
|
||||
{
|
||||
_zipIn.EncryptHeaders = true;
|
||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
||||
_zipIn.CompressStreamDictionary(filesStreams, target, _archivePassword);
|
||||
}
|
||||
ArchiveFileStream.Seek(0, SeekOrigin.Begin);
|
||||
FileStreamCreated?.Invoke();
|
||||
ExitOK = true;
|
||||
_zipIn = null;
|
||||
ZipInOK = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Previously silent: the DATEV export ZIP-to-stream path failed with no
|
||||
// trace anywhere, only a bare false return.
|
||||
_logger.LogError(ex, "Archive.CompressToStream failed — fileCount={FileCount}", files.Count);
|
||||
ExitOK = false;
|
||||
}
|
||||
return ExitOK;
|
||||
}
|
||||
|
||||
public bool WriteArchiveStreamToDisk(FileInfo? archiveFile = null)
|
||||
{
|
||||
try { if (_archiveFile.Exists) _archiveFile.Delete(); } catch { }
|
||||
FdsShared.WriteStreamToDisk(ArchiveFileStream!, (archiveFile ?? _archiveFile).FullName);
|
||||
_archiveFile.Refresh();
|
||||
return _archiveFile.Exists;
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
private bool _disposed;
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
ArchiveFileStream?.Dispose();
|
||||
_zipOut?.Dispose();
|
||||
_zipIn = null;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class Zipping
|
||||
{
|
||||
public static string SevenZipPath = "";
|
||||
|
||||
public static void FastAppend(FileInfo fileToZip, FileInfo archiveFile)
|
||||
{
|
||||
if (fileToZip.Exists && archiveFile?.Exists == true)
|
||||
{
|
||||
using var zip = new Archive(archiveFile) { ZipAppend = true };
|
||||
zip.Compress(new List<FileInfo> { fileToZip });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>fds</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -19,29 +18,18 @@
|
||||
<NoWarn>1591;NU1608</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="install.bat" />
|
||||
<Content Include="un-install.bat" />
|
||||
<!-- Expose internal members (FdsConfig, FdsShared helpers) to the test project -->
|
||||
<InternalsVisibleTo Include="Fuchs.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||
<ProjectReference Include="..\OCORE\OCORE\OCORE.csproj" />
|
||||
<ProjectReference Include="..\OCORE_web\OCORE_web\OCORE_web.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="7z.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
|
||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<StartupObject>Sub Main</StartupObject>
|
||||
<RootNamespace>fds</RootNamespace>
|
||||
<MyType>Empty</MyType>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Configurations>db-dev.processweb.de;Debug;Release;server02.processweb.de</Configurations>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Import Include="System" />
|
||||
<Import Include="System.Data" />
|
||||
<Import Include="Microsoft.Data" />
|
||||
<Import Include="System.IO" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DefineDebug>false</DefineDebug>
|
||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="My Project\Application.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Application.myapp</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<Compile Update="My Project\Resources.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="My Project\Settings.Designer.vb">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="install.bat" />
|
||||
<None Update="My Project\Application.myapp">
|
||||
<Generator>MyApplicationCodeGenerator</Generator>
|
||||
<LastGenOutput>Application.Designer.vb</LastGenOutput>
|
||||
</None>
|
||||
<Content Include="un-install.bat" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
|
||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCMSsharp\OCMS\OCMS.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="7z.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fuchs_DataService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Updated packages -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- Compatible packages (kept) -->
|
||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23" />
|
||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
||||
<!-- New packages (needed for .NET 10) -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -18,38 +18,10 @@
|
||||
</member>
|
||||
<member name="T:fds.FdsConfig">
|
||||
<summary>
|
||||
Holds the application <see cref="T:Microsoft.Extensions.Configuration.IConfiguration"/> built from appsettings.json.
|
||||
Call <see cref="M:fds.FdsConfig.Initialize"/> once at startup before accessing <see cref="P:fds.FdsConfig.Current"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.Logging.FdsLoggerProvider">
|
||||
<summary>
|
||||
Writes log entries to Debug output and a rolling file.
|
||||
Database logging is wired up but disabled — set <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> to true to activate.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled">
|
||||
<summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
||||
</member>
|
||||
<member name="M:fds.Logging.FdsLogger.WriteToDatabase(System.String,System.String,System.Exception)">
|
||||
<summary>
|
||||
Prepared DB logging via fds__admin_logdebug.
|
||||
Enable by setting <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.PeriodicJobDefinition">
|
||||
<summary>
|
||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:fds.PeriodicJobDefinition.#ctor(System.String,System.TimeSpan,System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task})">
|
||||
<summary>
|
||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:fds.PeriodicHostedService">
|
||||
<summary>
|
||||
A <see cref="T:Microsoft.Extensions.Hosting.BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="T:System.Threading.PeriodicTimer"/>.
|
||||
Holds the application <see cref="T:Microsoft.Extensions.Configuration.IConfiguration"/> supplied by the host (the Fuchs web app).
|
||||
Fuchs_DataService is a library — it has no configuration of its own; the host owns the
|
||||
connection strings and MFR credentials and injects them via <see cref="M:fds.FdsConfig.Initialize(Microsoft.Extensions.Configuration.IConfiguration)"/>,
|
||||
which <c>Program.cs</c> calls once at startup before any <see cref="T:fds.FdsMfr"/> use.
|
||||
</summary>
|
||||
</member>
|
||||
</members>
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace fds.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Writes log entries to Debug output and a rolling file.
|
||||
/// Database logging is wired up but disabled — set <see cref="DatabaseLoggingEnabled"/> to true to activate.
|
||||
/// </summary>
|
||||
public sealed class FdsLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly string _logDirectory;
|
||||
|
||||
/// <summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
||||
public static bool DatabaseLoggingEnabled { get; set; } = false;
|
||||
|
||||
public FdsLoggerProvider(string? logDirectory = null)
|
||||
{
|
||||
_logDirectory = logDirectory
|
||||
?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmp");
|
||||
Directory.CreateDirectory(_logDirectory);
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName) =>
|
||||
new FdsLogger(categoryName, _logDirectory);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
internal sealed class FdsLogger : ILogger
|
||||
{
|
||||
private readonly string _categoryName;
|
||||
private readonly string _logDirectory;
|
||||
private static readonly Lock _fileLock = new();
|
||||
|
||||
internal FdsLogger(string categoryName, string logDirectory)
|
||||
{
|
||||
_categoryName = categoryName;
|
||||
_logDirectory = logDirectory;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
|
||||
Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
|
||||
string message = formatter(state, exception);
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
string levelTag = logLevel switch
|
||||
{
|
||||
LogLevel.Trace => "TRC",
|
||||
LogLevel.Debug => "DBG",
|
||||
LogLevel.Information => "INF",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
LogLevel.Critical => "CRT",
|
||||
_ => "???"
|
||||
};
|
||||
|
||||
string line = $"{timestamp} [{levelTag}] {_categoryName}: {message}";
|
||||
if (exception != null)
|
||||
line += $"\r\n Exception: {exception.Message}\r\n Stack: {exception.StackTrace}";
|
||||
|
||||
// Always emit to Debug output
|
||||
Debug.WriteLine(line);
|
||||
|
||||
// Always write to file
|
||||
string filename = logLevel >= LogLevel.Error ? "ErrorLog.txt" : "DebugLog.txt";
|
||||
AppendToFile(filename, line);
|
||||
|
||||
// Database logging — prepared, not activated
|
||||
if (FdsLoggerProvider.DatabaseLoggingEnabled)
|
||||
WriteToDatabase(_categoryName, message, exception);
|
||||
}
|
||||
|
||||
private void AppendToFile(string filename, string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_fileLock)
|
||||
File.AppendAllText(Path.Combine(_logDirectory, filename), line + "\r\n");
|
||||
}
|
||||
catch { /* never throw from logger */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepared DB logging via fds__admin_logdebug.
|
||||
/// Enable by setting <see cref="FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
||||
/// </summary>
|
||||
private static void WriteToDatabase(string codeReference, string message, Exception? exception)
|
||||
{
|
||||
// Activate by setting FdsLoggerProvider.DatabaseLoggingEnabled = true in appsettings / startup.
|
||||
//
|
||||
// using var con = new Microsoft.Data.SqlClient.SqlConnection(FdsConfig.FDSConnectionString());
|
||||
// using var cmd = new Microsoft.Data.SqlClient.SqlCommand(
|
||||
// "EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;", con);
|
||||
// cmd.Parameters.AddWithValue("@CodeReference", codeReference);
|
||||
// cmd.Parameters.AddWithValue("@ExceptionMessage", (object?)exception?.Message ?? DBNull.Value);
|
||||
// cmd.Parameters.AddWithValue("@StackTrace", (object?)exception?.StackTrace ?? DBNull.Value);
|
||||
// cmd.Parameters.AddWithValue("@Data", message);
|
||||
// con.Open();
|
||||
// cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public static class FdsLoggingExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddFdsLogging(this ILoggingBuilder builder, string? logDirectory = null)
|
||||
{
|
||||
builder.AddProvider(new FdsLoggerProvider(logDirectory));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace fds;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a named job with its own execution schedule for use with <see cref="PeriodicHostedService"/>.
|
||||
/// </summary>
|
||||
public sealed record PeriodicJobDefinition(
|
||||
string Name,
|
||||
TimeSpan Interval,
|
||||
Func<CancellationToken, Task> Execute);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="PeriodicTimer"/>.
|
||||
/// </summary>
|
||||
public sealed class PeriodicHostedService : BackgroundService
|
||||
{
|
||||
private readonly IReadOnlyList<PeriodicJobDefinition> _jobs;
|
||||
private readonly ILogger<PeriodicHostedService> _logger;
|
||||
|
||||
public PeriodicHostedService(IEnumerable<PeriodicJobDefinition> jobs, ILogger<PeriodicHostedService> logger)
|
||||
{
|
||||
_jobs = jobs.ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var jobTasks = _jobs.Select(job => RunJobAsync(job, stoppingToken)).ToList();
|
||||
await Task.WhenAll(jobTasks);
|
||||
}
|
||||
|
||||
private async Task RunJobAsync(PeriodicJobDefinition job, CancellationToken stoppingToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(job.Interval);
|
||||
_logger.LogInformation("Job '{Name}' scheduled with interval {Interval}.", job.Name, job.Interval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
_logger.LogDebug("Job '{Name}' starting.", job.Name);
|
||||
try
|
||||
{
|
||||
await job.Execute(stoppingToken);
|
||||
_logger.LogDebug("Job '{Name}' completed.", job.Name);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Job '{Name}' failed.", job.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"fuchs_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;",
|
||||
"fuchs_fds_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_fds;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;"
|
||||
},
|
||||
"Fds": {
|
||||
"ExecutionFrequency_Minutes": 15,
|
||||
"DebugDetails": true,
|
||||
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
|
||||
"MFR_Password": "0oT4G3H2",
|
||||
"MFR_host": "portal.mobilefieldreport.com"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Fuchs_Dataservice.exe install --autostart
|
||||
@@ -1 +0,0 @@
|
||||
Fuchs_Dataservice.exe uninstall
|
||||
Reference in New Issue
Block a user