Initial Commit after switching from SVN to git

This commit is contained in:
2026-05-03 01:43:52 +02:00
parent ab8638e5bb
commit a4284234b2
910 changed files with 359931 additions and 0 deletions
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- All application settings have been migrated to appsettings.json. -->
<configuration>
</configuration>
+151
View File
@@ -0,0 +1,151 @@
using System.Data;
using System.Diagnostics;
using Microsoft.Data.SqlClient;
namespace fds;
internal static class FdsDebug
{
[DebuggerStepThrough]
public static FileInfo LogFile(string fileName) =>
new(AppBaseDirectory()!.FullName + fileName);
[DebuggerStepThrough]
public static DirectoryInfo? AppBaseDirectory()
{
string path = AppDomain.CurrentDomain.BaseDirectory + "tmp" + Path.DirectorySeparatorChar;
var di = new DirectoryInfo(path);
if (di.Exists) return di;
if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory))
{
di.Create();
return di;
}
return null;
}
[DebuggerStepThrough]
public static void DebugLog_async(string codeReference, string sqlConnectionString,
Exception? exc = null, string data = "", object? context = null)
{
if (string.IsNullOrEmpty(codeReference) || string.IsNullOrEmpty(sqlConnectionString)) return;
try
{
Task.Run(() => DebugLog_sync(codeReference, sqlConnectionString, exc, data, context));
}
catch (Exception ex)
{
DebugLog_sync("fds_debug DebugLog_async", sqlConnectionString, ex);
}
}
[DebuggerStepThrough]
public static void DebugLog_sync(string codeReference, string sqlConnectionString,
Exception? exc = null, string data = "", object? context = null)
{
if (string.IsNullOrEmpty(codeReference) || string.IsNullOrEmpty(sqlConnectionString)) return;
using var con = new SqlConnection(sqlConnectionString);
DebugLog(codeReference, con, exc, data, context);
}
[DebuggerStepThrough]
public static void DebugLog(string codeReference, SqlConnection? sqlConnection,
Exception? exc = null, string data = "", object? context = null)
{
if (string.IsNullOrEmpty(codeReference) || sqlConnection is null) return;
string note = DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss") + " - " + codeReference;
try
{
try
{
var pl = new List<SqlParameter>
{
new("@CodeReference", codeReference),
new("@ExceptionMessage", exc != null ? (object)exc.Message : DBNull.Value),
new("@StackTrace", exc?.StackTrace != null ? (object)exc.StackTrace : DBNull.Value),
new("@data", (object?)data ?? DBNull.Value)
};
try
{
int w = 0;
if (sqlConnection.State == ConnectionState.Broken) sqlConnection.Close();
if (sqlConnection.State == ConnectionState.Connecting)
{
while (sqlConnection.State == ConnectionState.Connecting && w < 10)
{
Thread.Sleep(100);
w++;
}
}
else if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
w = 0;
while (sqlConnection.State != ConnectionState.Open && w < 10)
{
Thread.Sleep(100);
w++;
}
using var cmd = new SqlCommand(
"EXECUTE [dbo].[fds__admin_logdebug] @CodeReference,@ExceptionMessage,@StackTrace,@Data;",
sqlConnection);
cmd.Parameters.AddRange(pl.ToArray());
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
}
catch { /* swallow SQL errors */ }
}
catch { /* swallow DB errors */ }
if (exc != null)
note += $"\r\nException:{exc.Message}\r\nStack:{exc.StackTrace}".Replace("\n", "\n ");
if (!string.IsNullOrEmpty(data))
note += $"\r\nData:{data}".Replace("\n", "\n ");
note += "\r\n";
var debugLogfile = LogFile("DebugLog.txt");
if (debugLogfile.Directory?.Exists == true)
File.AppendAllText(debugLogfile.FullName, note);
}
catch { /* swallow logging errors */ }
finally
{
Console.Write(note);
Debug.WriteLine(note);
}
}
public static void DebugToFile(string note, string filename = "DebugLog.txt")
{
try
{
var debugLogfile = LogFile(filename);
if (debugLogfile.Directory?.Exists == true)
File.AppendAllText(debugLogfile.FullName, DateTime.Now + ": " + note + "\r\n");
}
catch { }
}
public static void DebugToFile(string codeReference, Exception? exc, string data, string filename = "DebugLog.txt")
{
string note = codeReference;
if (exc != null)
note += $"\r\nException:{exc.Message}\r\nStack:{exc.StackTrace}".Replace("\n", "\n ");
if (!string.IsNullOrEmpty(data))
note += $"\r\nData:{data}".Replace("\n", "\n ");
DebugToFile(note, filename);
}
// Convenience overload using default FDS connection string
public static void DebugLog(string codeReference, Exception? exc = null, string data = "",
object? context = null, bool executeAsync = true)
{
if (executeAsync)
DebugLog_async(codeReference, FdsShared.FDSConnectionString(), exc, data, context);
else
DebugLog_sync(codeReference, FdsShared.FDSConnectionString(), exc, data, context);
}
}
+90
View File
@@ -0,0 +1,90 @@
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);
await mfr.UpdateRequested_async(debug);
await mfr.GetInvoiceFiles_async(debug);
})
};
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!);
}
}
}
+459
View File
@@ -0,0 +1,459 @@
using System.Data;
using System.Text;
using System.Xml;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MFR_RESTClient;
using MFR_RESTClient.generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OCORE.SQL;
using static OCORE.SQL.sql;
using static OCORE.commons;
namespace fds;
public class FdsSqlOptions : sqloptions
{
private Dictionary<string, object>? _baseInfo;
public FdsSqlOptions(Dictionary<string, object>? dic = null)
{
_baseInfo = dic;
OnError = (procedure, ex, data) =>
FdsDebug.DebugLog(procedure, exc: ex, data: $"ctrl_nfo={dic}, sql={data}");
}
}
public class FdsMfr : IFdsMfr
{
private readonly ILogger<FdsMfr> _logger;
private readonly ILoggerFactory _loggerFactory;
public FdsMfr(ILogger<FdsMfr> logger, ILoggerFactory loggerFactory)
{
_logger = logger;
_loggerFactory = loggerFactory;
}
public enum UpdateNeed
{
Reset = 5,
Full = 2,
Short = 1,
None = 0
}
public async Task UpdateIfNecessary_async(bool debugDetails = false)
{
using var mfr = new FdsMfrClient(_loggerFactory);
try
{
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn - start awaited", filename: "DebugDetail.txt");
await mfr.Update__Entitytables(debugDetails);
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn - completed", filename: "DebugDetail.txt");
}
catch (Exception ex)
{
FdsDebug.DebugLog("UpdateIfNecessary_async - main unn", exc: ex);
if (debugDetails) FdsDebug.DebugToFile("UpdateIfNecessary_async - unn", exc: ex, data: "", filename: "DebugDetail.txt");
}
}
public async Task UpdateRequested_async(bool debugDetails = false)
{
using var mfr = new FdsMfrClient(_loggerFactory);
try
{
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn - start awaited", filename: "DebugDetail.txt");
await mfr.Update__EntityRequests(debugDetails);
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn - completed", filename: "DebugDetail.txt");
}
catch (Exception ex)
{
FdsDebug.DebugLog("UpdateRequested_async - main unn", exc: ex);
if (debugDetails) FdsDebug.DebugToFile("UpdateRequested_async - unn", exc: ex, data: "", filename: "DebugDetail.txt");
}
}
public async Task GetInvoiceFiles_async(bool debugDetails = false)
{
using var mfr = new FdsMfrClient(_loggerFactory);
try
{
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async - start", filename: "DebugDetail.txt");
var dtbl = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__fn_getMFRInvoicesWithoutfiles];",
FdsShared.FDSConnectionString(), SqlParameterList: null, options: new FdsSqlOptions());
if (dtbl.Count > 0)
{
foreach (DataRow ivrw in dtbl.DataTable.Rows)
{
string id = ivrw.nz("id"), docName = ivrw.nz("DocumentName"), fileurl = ivrw.nz("URI");
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(docName) && !string.IsNullOrEmpty(fileurl) && docName.EndsWith("pdf"))
{
var fl = mfr.GetFile(fileurl);
if (fl != null && fl.Length > 0)
{
try
{
await setSQLValue_async(
"EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;",
FdsShared.FDSConnectionString(),
SqlParameterList: new ParamList(
SQL_VarChar("@Id", id),
SQL_VarChar("@filename", docName),
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
options: new FdsSqlOptions());
}
catch (Exception fsex)
{
FdsDebug.DebugLog("GetInvoiceFiles_async - mfr storefile", exc: fsex);
}
}
}
}
}
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async - completed", filename: "DebugDetail.txt");
}
catch (Exception ex)
{
FdsDebug.DebugLog("GetInvoiceFiles_async - main unn", exc: ex);
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async", exc: ex, data: "", filename: "DebugDetail.txt");
}
}
public FileInfo? GetReportDoc(ref byte[]? file, string reportid, bool debugDetails = false)
{
var pl = new List<SqlParameter> { SQL_VarChar("@reportid", reportid) };
var sqldt = Task.Run(async () => await getSQLDatatable_async(
"EXECUTE [dbo].[fds__getReportDocument] @reportid;",
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
if (sqldt.Count > 0)
{
var irow = sqldt.FirstRow;
string fln = irow.nz("DocumentName");
byte[]? fl = null;
try
{
if (!string.IsNullOrEmpty(fln) && !irow.nbool("fds", false))
{
using var mfr = new FdsMfrClient(_loggerFactory);
fl = mfr.GetFile(irow.nz("URI"));
if (fl != null && fl.Length > 0)
{
try
{
Task.Run(async () => await setSQLValue_async(
"EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;",
FdsShared.FDSConnectionString(),
SqlParameterList: new ParamList(
SQL_VarChar("@Id", reportid),
SQL_VarChar("@filename", fln),
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
options: new FdsSqlOptions())).Wait();
}
catch (Exception fsex) { FdsDebug.DebugLog("getReportDoc - mfr storefile", exc: fsex); }
}
}
else
{
fl = irow.no("file", null) as byte[];
}
}
catch (Exception ex)
{
FdsDebug.DebugLog("getReportDoc - mfr", exc: ex);
if (debugDetails) FdsDebug.DebugToFile("getReportDoc - mfr", exc: ex, data: "", filename: "DebugDetail.txt");
}
if (fl != null)
{
file = fl;
string safeName = fln.Replace("<p>", "").Replace("</p>", "").Replace("=", "_")
.Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("<", "_").Replace("/", "_");
return new FileInfo(Path.GetTempPath() + safeName);
}
}
return null;
}
public FileInfo? GetFdsDoc(ref byte[]? file, string reportid, string type)
{
var pl = new List<SqlParameter> { SQL_VarChar("@type", type), SQL_VarChar("@reportid", reportid) };
var sqldt = Task.Run(async () => await getSQLDatatable_async(
"EXECUTE [dbo].[fds__getFDSDocument] @type, @reportid;",
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
if (sqldt.Count > 0)
{
var irow = sqldt.FirstRow;
byte[]? fl = irow.no("file", null) as byte[];
string fln = irow.nz("DocumentName");
if (fl != null)
{
file = fl;
string safeName = fln.Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("?", "_");
return new FileInfo(Path.GetTempPath() + safeName);
}
}
return null;
}
public string DATEV(DatevHeader header, DataTable tbl) =>
header.ToHeaderString() + "\r\n" +
tbl.ToCsv(includeHeaders: true, quoteStrings: false, fieldDelimiter: ";",
cultureinfo: new System.Globalization.CultureInfo("de-DE"), quoteHeader: false);
public static UpdateNeed UpdateNeedValue(string need) =>
Enum.Parse<UpdateNeed>(need);
public FileInfo? GetDatevZip(ref Stream? stream, DateTime tgtdate, string mode, string authUser, bool includeFiles, bool debugDetails = false)
{
using var mfr = new FdsMfrClient(_loggerFactory);
try
{
var dset = Task.Run(async () => await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getDatevExports] @tgtdate, @mode, @files, @authuser;",
FdsShared.FDSConnectionString(),
SqlParameterList: new List<SqlParameter>
{
SQL_Date("@tgtdate", tgtdate), SQL_VarChar("@mode", mode),
SQL_Bit("@files", includeFiles), SQL_VarChar("@authuser", authUser)
},
tablenames: new[] { "admin", "inv", "buchungen", "debitoren" },
options: new FdsSqlOptions())).Result;
var bediFiles = new List<DatevDocument>();
if (dset.Count >= 4)
{
var admin = dset.Tables("admin").Rows[0].toObjectDictionary();
DateTime startdate = (DateTime)admin["startdate"], enddate = (DateTime)admin["enddate"];
string register = tgtdate.ToString("yyyy\\/MM") + (mode.ToLower() == "w" ? "_w" + tgtdate.ToString("dd") : "");
var fls = new Dictionary<string, byte[]>();
var header = new DatevHeader
{
Formatkategorie = DatevFormatkategorie.Buchungsstapel,
Formatversion = DatevFormatversion.Buchungsstapel_9,
Beraternummer = (int)admin["beraternummer"],
Mandantennummer = (int)admin["mandantennummer"],
WJBeginn = (DateTime)admin["WJ-Beginn"],
Sachkontenlänge = (int)admin["Sachkontenlänge"],
DatumVon = startdate,
DatumBis = enddate,
Bezeichnung = "fds_" + mode + tgtdate.ToString("yyMMdd")
};
fls.Add($"EXTF_PCW_Buchungen_0_{startdate:yyMMdd HHmmss}.csv",
DATEV(header, dset.Tables("buchungen")).ToByteArray(encoding: Encoding.GetEncoding("ISO-8859-1")));
header.Formatkategorie = DatevFormatkategorie.Debitoren__Kreditoren;
header.Formatversion = DatevFormatversion.Debitoren__Kreditoren;
fls.Add($"EXTF_PCW_Debitoren_0_{startdate:yyMMdd HHmmss}.csv",
DATEV(header, dset.Tables("debitoren")).ToByteArray(encoding: Encoding.GetEncoding("ISO-8859-1")));
foreach (var fl in fls.Keys)
bediFiles.Add(new DatevDocument(Guid.NewGuid().ToString().ToLower(), fl, "", "Processweb_Daten", register));
if (includeFiles)
{
foreach (DataRow irow in dset.Tables("inv").Rows)
{
byte[]? fl = null;
string fln = irow.nz("DocumentName");
try
{
if (!string.IsNullOrEmpty(fln) && !irow.nbool("fds", true))
fl = mfr.GetFile(irow.nz("URI"));
else
fl = irow.no("file", null) as byte[];
}
catch { }
if (fl != null && !string.IsNullOrEmpty(fln))
{
fls.Add(fln, fl);
bediFiles.Add(new DatevDocument((string)irow["file_guid"], fln,
$"RgNr: {irow.nz("InvoiceId")} RgDatum: {irow.ndt_string("DateOfCreation", "dd.MM.yyyy")}",
"ProcessWeb_Belege", register));
}
}
fls.Add("document.xml", CreateDatevDocumentXml(bediFiles).ToByteArray());
}
if (fls.Count > 0)
{
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;
}
}
catch { }
}
}
}
catch (Exception ex)
{
FdsDebug.DebugLog("getDatevZip - mfr", exc: ex);
if (debugDetails) FdsDebug.DebugToFile("getDatevZip - mfr", exc: ex, data: "", filename: "DebugDetail.txt");
}
return null;
}
public string CreateDatevDocumentXml(List<DatevDocument> files)
{
var xmldoc = new XmlDocument();
var xmlDecl = xmldoc.CreateXmlDeclaration("1.0", "UTF-8", null);
xmldoc.InsertBefore(xmlDecl, xmldoc.DocumentElement);
var rootNode = xmldoc.CreateElement("archive", "http://xml.datev.de/bedi/tps/document/v04.0");
xmldoc.AppendChild(rootNode);
var schemaLoc = xmldoc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance");
schemaLoc.Value = "http://xml.datev.de/bedi/tps/document/v04.0 document_v040.xsd";
rootNode.Attributes.Append(schemaLoc);
rootNode.SetAttribute("generatingSystem", "ProcessWeb");
rootNode.SetAttribute("version", "4.0");
var headerElem = xmldoc.CreateElement("header");
xmldoc.DocumentElement!.PrependChild(headerElem);
var dateElem = xmldoc.CreateElement("date");
dateElem.InnerText = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
headerElem.AppendChild(dateElem);
var descElem = xmldoc.CreateElement("description");
descElem.InnerText = "DATEV Schnittstelle ProcessWeb";
headerElem.AppendChild(descElem);
var content = xmldoc.CreateElement("content");
xmldoc.DocumentElement.AppendChild(content);
foreach (var fl in files)
{
var docElem = xmldoc.CreateElement("document");
docElem.SetAttribute("guid", fl.Guid);
docElem.SetAttribute("type", fl.Type.ToString());
docElem.SetAttribute("processID", fl.ProcessID.ToString());
if (!string.IsNullOrEmpty(fl.KeyWords))
{
var kw = xmldoc.CreateElement("keywords");
kw.InnerText = fl.KeyWords;
docElem.AppendChild(kw);
}
var extension = xmldoc.CreateElement("extension");
var extAttr = xmldoc.CreateAttribute("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance");
extAttr.Value = "File";
extension.Attributes.Append(extAttr);
extension.SetAttribute("name", fl.Filename);
docElem.AppendChild(extension);
var repo = xmldoc.CreateElement("repository");
void AddLevel(string id, string name)
{
var level = xmldoc.CreateElement("level");
level.SetAttribute("id", id);
level.SetAttribute("name", name);
repo.AppendChild(level);
}
AddLevel("1", "ProcessWeb");
AddLevel("2", fl.Repository);
AddLevel("3", fl.Register);
docElem.AppendChild(repo);
content.AppendChild(docElem);
}
using var sw = new StringWriter();
using (var tw = XmlWriter.Create(sw))
{
xmldoc.WriteTo(tw);
tw.Flush();
}
return sw.GetStringBuilder().ToString();
}
}
// DATEV data classes and enums
public class DatevHeader
{
public DatevKennzeichen Kennzeichen { get; set; } = DatevKennzeichen.EXTF;
public int Versionsnummer { get; set; } = 700;
public DatevFormatkategorie Formatkategorie { get; set; } = DatevFormatkategorie.Debitoren__Kreditoren;
public DatevFormatversion Formatversion { get; set; } = DatevFormatversion.Debitoren__Kreditoren;
public int Beraternummer { get; set; }
public int Mandantennummer { get; set; }
public DateTime WJBeginn { get; set; }
public int Sachkontenlänge { get; set; }
public DateTime DatumVon { get; set; }
public DateTime DatumBis { get; set; }
public string Bezeichnung { get; set; } = "";
public DatevBuchungstyp Buchungstyp { get; set; } = DatevBuchungstyp.Finanzbuchführung;
public DatevRechnungslegungszweck Rechnungslegungszweck { get; set; } = DatevRechnungslegungszweck.unabhängig;
public bool Festschreibung { get; set; } = true;
public string WKZ { get; set; } = "EUR";
public DateTime Created { get; set; } = DateTime.Now;
public string Formatname => Enum.GetName(Formatkategorie)!.Replace("__", "/").Replace("_", " ");
public string ToHeaderString()
{
var sb = new List<string>
{
$"\"{Enum.GetName(Kennzeichen)}\"",
Versionsnummer.ToString(),
((int)Formatkategorie).ToString(),
$"\"{Formatname}\"",
((int)Formatversion).ToString(),
Created.ToString("yyyyMMddHHmmssfff"),
"", "\"\"", "\"\"", "\"\"",
Beraternummer.ToString(),
Mandantennummer.ToString(),
WJBeginn.ToString("yyyyMMdd"),
Sachkontenlänge.ToString(),
DatumVon.ToString("yyyyMMdd"),
DatumBis.ToString("yyyyMMdd"),
$"\"{Bezeichnung}\"",
"\"\"",
((int)Buchungstyp).ToString(),
((int)Rechnungslegungszweck).ToString(),
Festschreibung ? "1" : "0",
$"\"{WKZ}\"",
"", "", "", "", "", "", "", ""
};
return string.Join(";", sb);
}
}
public enum DatevRechnungslegungszweck { unabhängig = 0, Steuerrecht = 30, Kalkulatorik = 40, Handelsrecht = 50, IFRS = 64 }
public enum DatevBuchungstyp { Finanzbuchführung = 1, Jahresabschluss = 2 }
public enum DatevKennzeichen { EXTF, DTVF }
public enum DatevFormatkategorie { Debitoren__Kreditoren = 16, Sachkontenbeschriftungen = 20, Buchungsstapel = 21, Zahlungsbedingungen = 46, Diverse_Adressen = 48, Wiederkehrende_Buchungen = 65 }
public enum DatevFormatversion { Debitoren__Kreditoren = 5, Sachkontenbeschriftungen = 3, Buchungsstapel_9 = 9, Buchungsstapel = 12, Zahlungsbedingungen = 2, Wiederkehrende_Buchungen = 4, Diverse_Adressen = 2 }
public class DatevDocument
{
public string Guid { get; set; }
public int Type { get; set; } = 2;
public int ProcessID { get; set; } = 2;
public string Filename { get; set; }
public string KeyWords { get; set; }
public string Repository { get; set; }
public string Register { get; set; }
public DatevDocument(string guid, string filename, string keywords, string repository, string register)
{
Guid = guid;
Filename = filename;
KeyWords = keywords;
Repository = repository;
Register = register;
}
}
+474
View File
@@ -0,0 +1,474 @@
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MFR_RESTClient;
using MFR_RESTClient.generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OCORE.SQL;
using static OCORE.SQL.sql;
using static OCORE.commons;
namespace fds;
public class FdsMfrClient : IDisposable
{
private readonly MFRClient _mfrClient;
private readonly ILogger<FdsMfrClient> _logger;
private static MFRClientCredentials DefaultCredentials() =>
new(FdsConfig.MFR_UserName, FdsConfig.MFR_Password);
public FdsMfrClient(ILoggerFactory? loggerFactory = null, MFRClientCredentials? credentials = null)
{
_logger = loggerFactory?.CreateLogger<FdsMfrClient>()
?? Microsoft.Extensions.Logging.Abstractions.NullLogger<FdsMfrClient>.Instance;
var config = new MFRClientConfig(FdsConfig.MFR_host);
_mfrClient = new MFRClient(config, credentials ?? DefaultCredentials(),
loggerFactory?.CreateLogger<MFRClient>());
}
public bool IsReadonly
{
get => _mfrClient.IsReadonly;
set => _mfrClient.IsReadonly = value;
}
public MFRClientConfig ClientConfig => _mfrClient.ClientConfig;
public async Task<string> ReadAnything(string address, bool throwErrorIfNotOk = true) =>
await _mfrClient.ReadAnything(address, throwErrorIfNotOk);
public byte[]? GetFile(string address, bool throwErrorIfNotOk = true) =>
_mfrClient.GetFile(address, throwErrorIfNotOk);
public async Task<ODataEnvelope> ReadOData(string address, bool throwErrorIfNotOk = true) =>
await _mfrClient.ReadOData(address, throwErrorIfNotOk);
public async Task<string> GetEntities(bool throwErrorIfNotOk = true) =>
await _mfrClient.GetEntities(throwErrorIfNotOk);
// Database schema cache for MFR entity tables
public class DatabaseSchema
{
private readonly EntityTypes _et;
public bool IsValid { get; }
public bool HasEntity { get; }
public string ThisEntityName => EntityHelper.EntityName(_et);
public Dictionary<string, string> EntityConfig { get; }
public string EntityTableName { get; }
public Dictionary<string, Dictionary<string, string>> ComplexTypes { get; } = new();
public Dictionary<string, Dictionary<string, string>> NavProperties { get; } = new();
private readonly DataSet _tableSet;
public DatabaseSchema(EntityTypes et)
{
_et = et;
var dset = Task.Run(async () => await getSQLDataSet_async(
"EXECUTE [dbo].[mfr__getSchema] 'table', @tgttype;",
FdsShared.FDSConnectionString(),
SqlParameterList: new List<SqlParameter> { new("@tgttype", ThisEntityName) },
tablenames: new[] { "entity", "complex_types", "navigation_properties", "tables" },
options: new FdsSqlOptions())).Result;
IsValid = dset.Count > 0;
HasEntity = dset.Tables("entity").Rows.Count == 1;
EntityConfig = dset.Tables("entity").Rows.Count > 0
? dset.Tables("entity").Rows[0].toStringDictionary()
: new Dictionary<string, string>();
EntityTableName = EntityConfig.TryGetValue("tablename", out var tn) ? tn : "";
foreach (DataRow ctrw in dset.Tables("complex_types").Rows)
ComplexTypes[ctrw.nz("name").ToLower()] = ctrw.toStringDictionary();
foreach (DataRow nprw in dset.Tables("navigation_properties").Rows)
NavProperties[nprw.nz("name").ToLower()] = nprw.toStringDictionary();
var sqlList = new List<string> { NewDatatableSql(EntityTableName) };
var tableNames = new List<string> { EntityTableName };
foreach (DataRow ttrw in dset.Tables("tables").Rows)
{
sqlList.Add(NewDatatableSql(ttrw.nz("tablename")));
tableNames.Add(ttrw.nz("tablename"));
}
_tableSet = Task.Run(async () => await getSQLDataSet_async(
string.Join(Environment.NewLine, sqlList),
FdsShared.FDSConnectionString(),
tablenames: tableNames.ToArray(),
options: new FdsSqlOptions())).Result.DataSet;
}
public DataSet TgtDataset(string setId)
{
var dset = new DataSet();
foreach (DataTable tbl in _tableSet.Tables)
{
var t = tbl.Clone();
if (t.Columns.Contains("setid")) t.Columns["setid"]!.DefaultValue = setId;
dset.Tables.Add(t);
}
return dset;
}
}
public async Task<bool> Update__entitytable(EntityTypes et, FdsMfr.UpdateNeed updateNeed,
long[]? entityId = null, bool debugDetails = false,
Dictionary<string, DatabaseSchema>? schemaDic = null,
string additionalCommandAfter = "")
{
if (et == EntityTypes.none) return false;
entityId ??= Array.Empty<long>();
if (updateNeed == FdsMfr.UpdateNeed.None && entityId.Length == 0) return true;
var startTime = DateTime.Now;
_logger.LogDebug("Start before Schema {EntityType}", et);
string setId = FdsShared.RandomString(5);
string thisEntityName = EntityHelper.EntityName(et);
Action<string, string, string> dtf = (note, info, data) =>
{
if (debugDetails) FdsDebug.DebugToFile($"Update__entitytable - {note} For {thisEntityName}({setId}){(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt");
};
Action<string, string, string> dlg = (note, info, data) =>
{
string str = $"Update__entitytable - {note} for {thisEntityName}({setId}){(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
_logger.LogError("{Message} | data={Data}", str, data);
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt");
};
// Check lock (simplified — original lock logic was commented out)
object lockstate = true;
if (debugDetails && lockstate is bool) FdsDebug.DebugToFile($"Update__entitytable - lock received for {thisEntityName}({setId}) - {lockstate}", filename: "DebugDetail.txt");
if (lockstate is bool b && b)
{
try
{
var schema = schemaDic != null && schemaDic.TryGetValue(thisEntityName, out var s) ? s : new DatabaseSchema(et);
if (!schema.IsValid)
{
dlg("Schema not found", "", "");
}
else if (schema.HasEntity)
{
var tgtDataset = schema.TgtDataset(setId);
var filter = new List<string>();
string entityIdFilter = "";
bool isProcessed = false;
if (entityId.Length == 1)
{
entityIdFilter = $"({entityId[0]}L)";
filter.Add("");
updateNeed = FdsMfr.UpdateNeed.Reset;
}
else if (entityId.Length > 1)
{
for (int idx = 0; idx < entityId.Length; idx += 50)
filter.Add("$filter=" + string.Join(" or ", entityId.Skip(idx).Take(50).Select(l => $"Id eq {l}L")));
updateNeed = FdsMfr.UpdateNeed.Reset;
}
else if (updateNeed > FdsMfr.UpdateNeed.Short)
{
filter.Add("");
}
else if (schema.EntityConfig.ContainsKey("DateColumn") && !string.IsNullOrEmpty(schema.EntityConfig["DateColumn"]))
{
var lastDate = await getSQLValue_async(
schema.EntityConfig.nz("DateSQL").ne($"SELECT MAX([{schema.EntityConfig["DateColumn"]}]) FROM [dbo].[{schema.EntityTableName}];"),
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
filter.Add(
updateNeed == FdsMfr.UpdateNeed.Short && lastDate.Result is DateTime dt
? $"$filter={schema.EntityConfig["DateColumn"]} gt DateTime'{dt:yyyy-MM-dd}T00:00:00'"
: "");
}
foreach (var iFilter in filter)
{
if (schema.EntityConfig.ContainsKey("url") && !string.IsNullOrEmpty(schema.EntityConfig["url"])
&& ((updateNeed == FdsMfr.UpdateNeed.Short && !string.IsNullOrEmpty(iFilter)) || updateNeed > FdsMfr.UpdateNeed.Short))
{
isProcessed = true;
_logger.LogDebug("Start MFR {Table}", schema.EntityTableName);
string url = _mfrClient.ClientConfig.BaseUrl + schema.EntityConfig["url"].Replace("?", entityIdFilter + "?")
+ (string.IsNullOrEmpty(iFilter) ? "" : (!schema.EntityConfig["url"].Contains('?') ? "?" : "&") + iFilter);
var mfrResponse = await ReadOData(url);
_logger.LogDebug("End MFR {Table}", schema.EntityTableName);
while (mfrResponse != null)
{
if (mfrResponse.Value is JObject) mfrResponse.ConvertToArray();
try
{
if (mfrResponse.Value is JArray ja && ja.First is JContainer jc && jc.First is JProperty jp
&& jp.Name == "odata.error" && ja.First.ToString().Contains("Sequence contains no elements", StringComparison.OrdinalIgnoreCase))
mfrResponse.Value = new JArray();
}
catch { }
if (mfrResponse.Value is JArray valueArray)
{
_logger.LogDebug("MFR rows: received={Received}, local={Local}", valueArray.Count, tgtDataset.Tables[schema.EntityTableName]!.Rows.Count);
foreach (JObject vi in valueArray)
{
var vdic = vi.ToObject<Dictionary<string, object>>()!;
var etrow = tgtDataset.Tables[schema.EntityTableName]!.NewRow();
etrow["setid"] = setId;
etrow["Id"] = vdic["Id"];
foreach (var vkey in vdic.Keys)
{
if (vdic[vkey] == null) continue;
if (schema.ComplexTypes.ContainsKey(vkey.ToLower()) && schema.ComplexTypes[vkey.ToLower()]["name"] != "CustomValues")
StoreCP(tgtDataset, etrow, schema.ComplexTypes, vdic[vkey], $"{thisEntityName}:{vkey}", vkey, setId, vdic);
else if (schema.ComplexTypes.ContainsKey(vkey.ToLower()) && schema.ComplexTypes[vkey.ToLower()]["name"] == "CustomValues")
{
if (etrow.Table.Columns.Contains(vkey)) etrow[vkey] = JsonConvert.SerializeObject(vdic[vkey]);
}
else if (schema.NavProperties.ContainsKey(vkey.ToLower()))
StoreNP(tgtDataset, schema.ComplexTypes, schema.NavProperties[vkey.ToLower()], vdic[vkey], $"{thisEntityName}:{vkey}", setId, vdic);
else
SetRowValue(etrow, vkey, vdic[vkey]);
}
tgtDataset.Tables[schema.EntityTableName]!.Rows.Add(etrow);
tgtDataset.Tables[schema.EntityTableName]!.AcceptChanges();
}
}
if (mfrResponse.NextLink != null && !string.IsNullOrEmpty(mfrResponse.NextLink.ToString()))
mfrResponse = await ReadOData(mfrResponse.NextLink.ToString());
else
mfrResponse = null!;
}
}
}
_logger.LogDebug("Start SQL {Table}", schema.EntityTableName);
if (isProcessed)
{
foreach (DataTable tbl in tgtDataset.Tables)
{
int trc = tbl.Rows.Count;
if (trc > 0 || entityId.Length > 0)
{
try
{
tbl.AcceptChanges();
var dtwa = new DatatableWriterAsync(tbl, FdsShared.FDSConnectionString());
dtwa.CommandAfterError = new SqlCommand($"SELECT * INTO [t_{setId.ToLower()}_{tbl.TableName}] FROM {dtwa.DestinationTableName};");
dtwa.CommandAfter = new SqlCommand(
$"EXECUTE [dbo].[{tbl.TableName.Replace("__", "__updt__")}] @tblname, @referencetable, @tgtid;" +
"EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" +
(additionalCommandAfter ?? ""));
dtwa.CommandAfter.Parameters.AddRange(new SqlParameter[]
{
new("@tblname", dtwa.DestinationTableName),
new("@table", tbl.TableName),
new("@action", (tbl.TableName.Equals(schema.EntityTableName, StringComparison.OrdinalIgnoreCase) ? "update_" : "imdu_") + updateNeed.ToString().ToLower()),
new("@setid", setId.ToLower()),
new("@entityname", schema.ThisEntityName),
new("@info", JsonConvert.SerializeObject(new { count = trc, reference = schema.EntityTableName, id = entityId.Length > 0 ? string.Join(",", entityId) : (object?)null })),
new("@referencetable", schema.EntityTableName),
new("@tgtid", entityId.Length > 0 ? entityId[0].ToString() : (object)DBNull.Value)
});
dtwa.DoSubmit();
}
catch (Exception dsex)
{
dlg("Submission issue", dsex.Message, $"table: {tbl.TableName}");
}
}
}
}
}
}
catch (Exception ex)
{
dlg(ex.Message, "", ex.StackTrace ?? "");
}
finally
{
if (debugDetails) FdsDebug.DebugToFile($"Update__entitytable - lock released for {thisEntityName}({setId})", filename: "DebugDetail.txt");
}
}
return true;
}
private static void SetRowValue(DataRow row, string key, object value)
{
if (!row.Table.Columns.Contains(key)) return;
try
{
if (value is DateTime dt) { if (dt.Year > 1900) row[key] = value; }
else if (value.GetType() == row.Table.Columns[key]!.DataType) row[key] = value;
else if (value is string s && value.GetType() != row.Table.Columns[key]!.DataType)
{
try { row[key] = GenericExtensions.TryConvert(s, row.Table.Columns[key]!.DataType); }
catch { try { row[key] = value; } catch { } }
}
else
{
try { row[key] = value; } catch { }
}
}
catch { }
}
private void StoreCP(DataSet tgtDataset, DataRow etrow, Dictionary<string, Dictionary<string, string>> complexTypes,
object cp, string entityProperty, string vkey, string setId, Dictionary<string, object> vdic)
{
if (cp is JArray arr)
{
foreach (JObject vitm in arr) StoreCP(tgtDataset, etrow, complexTypes, vitm, entityProperty, vkey, setId, vdic);
}
else if (cp is JObject jObj)
{
var cxType = complexTypes[vkey.ToLower()];
var cto = jObj.ToObject<Dictionary<string, object>>()!;
var cnr = tgtDataset.Tables[cxType["tablename"]]!.NewRow();
cnr["setid"] = setId;
foreach (var cky in cto.Keys)
{
if (cto[cky] != null && cnr.Table.Columns.Contains(cky))
SetRowValue(cnr, cky, cto[cky]);
}
if (cnr.Table.Columns.Contains("EntityId")) cnr["EntityId"] = vdic["Id"];
if (cnr.Table.Columns.Contains("Property")) cnr["Property"] = entityProperty;
tgtDataset.Tables[cxType["tablename"]]!.Rows.Add(cnr);
if (etrow.Table.Columns.Contains(vkey + "#ID") && cto.ContainsKey("id")) etrow[vkey + "#ID"] = vdic["Id"];
}
}
private void StoreNP(DataSet tgtDataset, Dictionary<string, Dictionary<string, string>> complexTypes,
Dictionary<string, string> navProp, object np, string entityProperty, string setId, Dictionary<string, object> vdic)
{
if (np is JArray arr)
{
foreach (JObject vitm in arr) StoreNP(tgtDataset, complexTypes, navProp, vitm, entityProperty, setId, vdic);
}
else if (np is JObject jObj)
{
var nto = jObj.ToObject<Dictionary<string, object>>()!;
if (nto.ContainsKey("Id"))
{
var nnr = tgtDataset.Tables[navProp["countertable"]]!.NewRow();
var ncr = tgtDataset.Tables[navProp["tablename"]]!.NewRow();
nnr["setid"] = setId;
foreach (var cky in nto.Keys)
{
if (nto[cky] == null) continue;
if (complexTypes.ContainsKey(cky.ToLower()) && complexTypes[cky.ToLower()]["name"] != "CustomValues")
StoreCP(tgtDataset, nnr, complexTypes, nto[cky], $"{complexTypes[cky.ToLower()]["EntityType"]}:{cky}", cky, setId, vdic);
else if (nnr.Table.Columns.Contains(cky))
SetRowValue(nnr, cky, nto[cky]);
}
ncr["EntityId"] = vdic["Id"];
ncr["PartnerType"] = navProp["countertype"];
ncr["PartnerId"] = nto["Id"];
ncr["Property"] = entityProperty;
tgtDataset.Tables[navProp["tablename"]]!.Rows.Add(ncr);
tgtDataset.Tables[navProp["countertable"]]!.Rows.Add(nnr);
}
}
}
private static string NewDatatableSql(string tablename) =>
$"Select TOP(0) [setid] = CAST('' as varchar(50)), * FROM [dbo].[{tablename}];";
public async Task Update__Entitytables(bool debugDetails = false, EntityTypes? tgtEntityType = null)
{
Action<string, string, string, Exception?> dtf = (note, info, data, ex) =>
{
if (debugDetails) FdsDebug.DebugToFile($"Update__AllEntitytables - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt", exc: ex, data: data);
};
Action<string, string, string, Exception?> dlg = (note, info, data, ex) =>
{
string str = $"Update__AllEntitytables - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
_logger.LogError(ex, "{Message} | data={Data}", str, data);
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt", exc: ex, data: data ?? "");
};
try
{
string sql = "SELECT * FROM [dbo].[fds__getUpdateableTables]()"
+ (tgtEntityType.HasValue ? $" WHERE [entity_name] = '{tgtEntityType.Value}'" : "") + ";";
var updateableTables = await getSQLDatatable_async(sql, FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
dtf("UpdateableTables", updateableTables.Exception ?? "", $"({(updateableTables.Count > 0 ? updateableTables.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
if (updateableTables.Count > 0 && updateableTables.DataTable.Columns.Contains("updateneed"))
{
foreach (DataRow rw in updateableTables.Select("updateneed > 0", "updateneed DESC"))
{
string etname = rw.nz("entity_name", "");
try
{
if (string.IsNullOrEmpty(etname))
dtf("no entity_name received", "", JsonConvert.SerializeObject(rw.ItemArray), null);
else if (!rw.nbool("locked", true))
{
var tblUpdateNeed = (FdsMfr.UpdateNeed)rw.nint64("updateneed", 0);
var et = EntityHelper.EntityValue(etname);
if (et != EntityTypes.none)
{
dtf($"updating: {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), null);
await Update__entitytable(et, updateNeed: tblUpdateNeed, debugDetails: debugDetails);
dtf("updating task completed", "", "", null);
}
else dtf("entitytype not known", "", $"etname={etname}", null);
}
else dtf($"Table update locked {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), null);
}
catch (Exception tableUpdateEx) { dlg($"updatepart failed - {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), tableUpdateEx); }
}
}
}
catch (Exception exa) { dlg("outer frame", "", "", exa); }
}
public async Task Update__EntityRequests(bool debugDetails = false)
{
Action<string, string, string, Exception?> dtf = (note, info, data, ex) =>
{
if (debugDetails) FdsDebug.DebugToFile($"Update__EntityRequests - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}", filename: "DebugDetail.txt", exc: ex, data: data);
};
Action<string, string, string, Exception?> dlg = (note, info, data, ex) =>
{
string str = $"Update__EntityRequests - {note}{(string.IsNullOrEmpty(info) ? "" : ": " + info)}";
_logger.LogError(ex, "{Message} | data={Data}", str, data);
if (debugDetails) FdsDebug.DebugToFile(str, filename: "DebugDetail.txt", exc: ex, data: data ?? "");
};
try
{
var updateableRequests = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fds__getUpdateableRequests]();",
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
dtf("UpdateableRequests", updateableRequests.Exception ?? "", $"({(updateableRequests.Count > 0 ? updateableRequests.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
if (updateableRequests.Count > 0)
{
foreach (DataRow rw in updateableRequests.Select("", "order"))
{
string etname = rw.nz("entity_name", "");
long tgtid = rw.nint64("Id", -1);
if (tgtid > -1 && !string.IsNullOrWhiteSpace(etname))
{
try
{
var et = EntityHelper.EntityValue(etname);
await Update__entitytable(et, updateNeed: FdsMfr.UpdateNeed.Reset,
entityId: new[] { tgtid }, debugDetails: debugDetails,
additionalCommandAfter: "DELETE FROM [dbo].[fds__mfr_updaterequests] WHERE LOWER([entity_name]) = LOWER(ISNULL(@entityname,'')) AND CAST(ISNULL([Id],'') as varchar(1000)) = @tgtid;");
}
catch (Exception tex) { dlg($"Update__EntityRequest single failed - {etname}", "", JsonConvert.SerializeObject(rw.ItemArray), tex); }
}
}
}
}
catch (Exception exa) { dlg("outer frame", "", "", exa); }
}
public void Dispose()
{
(_mfrClient as IDisposable).Dispose();
}
}
+183
View File
@@ -0,0 +1,183 @@
using System.Data;
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"/>.
/// </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();
}
// -- 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");
internal static string MFR_UserName =>
Current["Fds:MFR_UserName"] ?? "";
internal static string MFR_Password =>
Current["Fds:MFR_Password"] ?? "";
internal static string MFR_host =>
Current["Fds:MFR_host"] ?? "";
}
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();
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var sb = new StringBuilder();
for (byte i = 1; i <= length; i++)
{
int idx = r.Next(0, chars.Length);
sb.Append(chars[idx]);
}
return sb.ToString();
}
public static string ToCsv(this DataRow source, bool quoteStrings, CultureInfo cultureinfo, string delimiter = ",")
{
var fieldValues = source.ItemArray;
var rx = new Regex("(\")");
var converted = fieldValues!.Select(o =>
{
if (o is null or DBNull) return "";
if (o is string s)
return quoteStrings ? "\"" + rx.Replace(s, "\"\"") + "\"" : s;
return o switch
{
decimal d => d.ToString(cultureinfo),
float f => f.ToString(cultureinfo),
double db => db.ToString(cultureinfo),
bool b => b.ToString(cultureinfo),
DateTime dt => dt.ToUniversalTime().ToString("U"),
_ => o.ToString() ?? ""
};
}).ToArray();
return string.Join(delimiter, converted);
}
public static string ToCsv(this DataTable source, bool includeHeaders, bool quoteStrings,
string rowDelimiter = "\r\n", string fieldDelimiter = ",",
CultureInfo? cultureinfo = null, bool? quoteHeader = null)
{
quoteHeader ??= quoteStrings;
cultureinfo ??= CultureInfo.InvariantCulture;
var rows = source.Rows.Cast<DataRow>()
.Select(row => row.ToCsv(quoteStrings, cultureinfo, delimiter: fieldDelimiter));
if (includeHeaders)
{
var rx = new Regex("(\")");
var headers = string.Join(fieldDelimiter,
source.Columns.Cast<DataColumn>()
.Select(col => quoteHeader.Value
? "\"" + rx.Replace(col.ColumnName, "\"\"") + "\""
: col.ColumnName));
rows = new[] { headers }.Concat(rows);
}
return string.Join(rowDelimiter, rows);
}
public static byte[] ToByteArray(this string input, Encoding? encoding = null)
{
using var ms = new MemoryStream();
using (var sw = new StreamWriter(ms, encoding: encoding ?? Encoding.UTF8, leaveOpen: true))
{
sw.Write(input);
sw.Flush();
}
return ms.ToArray();
}
public static bool WriteStreamToDisk(Stream streamToWrite, string filePath)
{
int cnt = 0;
while (true)
{
try
{
if (File.Exists(filePath)) File.Delete(filePath);
using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Delete);
ReadWriteStream(streamToWrite, fs, true);
return true;
}
catch (Exception ex)
{
FdsDebug.DebugLog("FdsShared.WriteStreamToDisk", exc: ex, data: $"filePath={filePath}");
cnt++;
if (cnt == 6) return false;
Thread.Sleep(500);
}
}
}
public static bool ReadWriteStream(Stream readStream, Stream writeStream, bool closeWriteStream)
{
try
{
const int length = 256;
var buffer = new byte[length];
readStream.Seek(0, SeekOrigin.Begin);
int bytesRead = readStream.Read(buffer, 0, length);
while (bytesRead > 0)
{
writeStream.Write(buffer, 0, bytesRead);
bytesRead = readStream.Read(buffer, 0, length);
}
readStream.Close();
if (closeWriteStream) writeStream.Close();
return true;
}
catch (Exception ex)
{
FdsDebug.DebugLog("FdsShared.ReadWriteStream", exc: ex);
return false;
}
}
public static string NameBase(this FileInfo fi) =>
fi.Name[..^fi.Extension.Length];
}
+237
View File
@@ -0,0 +1,237 @@
using Microsoft.Extensions.Logging;
using SevenZip;
namespace fds;
public class Archive : IDisposable
{
public event Action? Saving;
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
{
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
{
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 });
}
}
}
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<RootNamespace>fds</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="install.bat" />
<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" />
</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.6" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.6" />
</ItemGroup>
</Project>
@@ -0,0 +1,76 @@
<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>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectView>ShowAllFiles</ProjectView>
<PublishUrlHistory />
<InstallUrlHistory />
<SupportUrlHistory />
<UpdateUrlHistory />
<BootstrapperUrlHistory />
<ErrorReportUrlHistory />
<FallbackCulture>en-US</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
</PropertyGroup>
</Project>
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Fuchs_DataService</name>
</assembly>
<members>
<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"/>.
</summary>
</member>
</members>
</doc>
+15
View File
@@ -0,0 +1,15 @@
using System.Data;
namespace fds;
public interface IFdsMfr
{
Task UpdateIfNecessary_async(bool debugDetails = false);
Task UpdateRequested_async(bool debugDetails = false);
Task GetInvoiceFiles_async(bool debugDetails = false);
FileInfo? GetReportDoc(ref byte[]? file, string reportid, bool debugDetails = false);
FileInfo? GetFdsDoc(ref byte[]? file, string reportid, string type);
FileInfo? GetDatevZip(ref Stream? stream, DateTime tgtdate, string mode, string authUser, bool includeFiles, bool debugDetails = false);
string DATEV(DatevHeader header, DataTable tbl);
string CreateDatevDocumentXml(List<DatevDocument> files);
}
@@ -0,0 +1,117 @@
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;
}
}
@@ -0,0 +1,53 @@
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);
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"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
View File
@@ -0,0 +1 @@
Fuchs_Dataservice.exe install --autostart
+1
View File
@@ -0,0 +1 @@
Fuchs_Dataservice.exe uninstall