Compare commits

...

73 Commits

Author SHA1 Message Date
b3c4d95c6a bump to 1.0.1 2025-08-27 16:16:56 +02:00
6a4817cbcd feat(Proxy): add logic tro enable swagger in production 2025-08-27 16:16:23 +02:00
a63bccf47d add ISS profiler 2025-08-19 09:17:11 +02:00
33df010573 Update zur Unterstützung mehrerer Frameworks (.NET 7, 8 und 9) 2025-08-18 17:52:32 +02:00
6433e0b39b add ExceptionHandlingMiddleware 2025-08-18 17:30:28 +02:00
c14e6033cb add NLog 2025-08-18 17:26:47 +02:00
df089af385 feat(EConnectClient): Curl-Logging für GET-Requests hinzugefügt
- Curl-Log wird jetzt auch bei `GetAsync` und `GetListAsAsyncEnumerable` ausgeführt.
- Bisher wurde Logging nur bei `PostAsync` durchgeführt.
- Hilft beim Debuggen von HTTP-Requests und beim Nachvollziehen von API-Aufrufen.
2025-08-18 17:21:12 +02:00
8b9f7b911d fix(LogExtensions): Fehlerbehandlung zu den LogCurl-Erweiterungsmethoden hinzufügen
Umschließt die LogCurl-Methoden mit try-catch-Blöcken, um zu verhindern, dass unerwartete
Ausnahmen die Anwendung beim Generieren oder Protokollieren von
cURL-Darstellungen von HTTP-Anfragen zum Absturz bringen. Fehler werden nun mit
logger.LogError protokolliert.
2025-08-18 17:14:44 +02:00
49c4960f05 refactor: Curl-Logging erweitert und String-Truncation eingeführt
- Entfernt manuelles Abschneiden von Curl-Strings und ersetzt durch die neue `Truncate`-Extension.
- Standardwert für `uri` im zweiten LogCurl-Overload auf "/" gesetzt.
- Verbessert Lesbarkeit und Wiederverwendbarkeit der Logik für Curl-Strings.
2025-08-18 17:11:14 +02:00
66cfe0525c perf(logging): use AsSpan for substring in LogCurl to reduce allocations
- Replaced `.Substring(0, maxLength)` with `AsSpan(0, maxLength)` and `string.Concat`.
- Improves performance by avoiding temporary string allocations when truncating cURL output.
- Maintains existing logging behavior with truncated cURL strings.
2025-08-18 16:49:49 +02:00
e9a7ef910f Refactoring (LogExtensions): Ersetzen der benutzerdefinierten LogCurlAsync durch eine auf HttpClientToCurl basierende Implementierung
- Manuelle cURL-Zeichenfolgenkonstruktion in `LogExtensions` entfernt.
- Überladungen von `LogCurl` unter Verwendung von `HttpClient.GenerateCurlInString` eingeführt.
- Unterstützung für das Kürzen langer cURL-Ausgaben mit `maxLength` hinzugefügt.
- Parameter vereinfacht und Wartbarkeit verbessert.
2025-08-18 16:48:23 +02:00
0273beb6f8 add to-do 2025-08-18 15:36:11 +02:00
20b5b8124d refactor(EConnectClient): Vereinfachung der Antwortverarbeitung in EConnectClient durch Verwendung von bedingten Ausdrücken und Entfernen der ungenutzten PostAsync-Überladung 2025-08-18 15:31:56 +02:00
beadc3c4bb refactor(EConnectClient): GetMimeType hinzufügen, um den MIME-Typ basierend auf dem Dateinamen zuzuweisen 2025-08-18 15:21:05 +02:00
9256dc6baf feat(econnect): Aktualisierung von PostAsync zur Verwendung des Inhaltstyps „application/pdf“ und Vereinfachung der Erstellung von Mehrteilformularen
- Generischer Header „multipart/form-data“ wurde durch „application/pdf“ für Dateiinhalte ersetzt
- Erstellung von Mehrteilformularen wurde zur Verbesserung der Übersichtlichkeit und Korrektheit umstrukturiert
- Es wurde sichergestellt, dass Dateistream und Dateiname direkt zum Mehrteilformular hinzuge
2025-08-18 15:08:31 +02:00
e8fd49d75d feat(PostDocumentRequest): update to forward stream instad of whole content 2025-08-18 14:53:20 +02:00
b5082daa1a feat(EConnectClient): update PostAsync to handle Stream instead of StreamContent 2025-08-18 14:46:00 +02:00
48a69f884e feat(LogExtensions): add full url and use placeholder for binary-data 2025-08-18 13:15:31 +02:00
b3a27ba24f feat(LogExtensions): Erstellen und Hinzufügen der Methode „LogCurlAsync“ zur Log-Curl-Methode der Anfrage 2025-08-18 12:42:16 +02:00
a7f02e1079 fix(OrderController): Aktualisierung, um StatusCodeInt als Statuscode zurückzugeben
- Liste der Pfad-Anforderungsrouten entfernen
2025-08-18 12:31:48 +02:00
265862d63d fix(EConnectClient): Behandlung leerer HTTP-Antwortinhalte
- Es wurden Überprüfungen für `ContentLength > 0` hinzugefügt, bevor Antwortinhalte in den Methoden GetAsync, GetListAsAsyncEnumerable und PostAsync deserialisiert werden.
- Stellt sicher, dass null zurückgegeben wird, anstatt zu versuchen, leere Antworten zu deserialisieren, wodurch potenzielle Laufzeitfehler vermieden werden.
2025-08-18 11:16:16 +02:00
060ba64268 feat(EConnectClient): Logik hinzufügen, um zu behandeln, wenn bei einem Fehler kein Inhalt vorhanden ist 2025-08-18 11:03:28 +02:00
21cc348c6c feat(PostDocumentRequest): Erstellen UploadDocumentRequest, um den Dokumenten-Upload-Prozess von PostDocumentRequest zu trennen.
- Aktualisieren den Controller, um den Upload-Prozess zu verarbeiten.
2025-08-18 10:48:15 +02:00
ccecf47dca feat(OrderController): add PostDocument to hand document upload process 2025-08-18 10:26:36 +02:00
6044d0bcb6 add PostDocumentRequest 2025-08-15 15:41:53 +02:00
aaaaf283ee add method to post StreamContent 2025-08-15 15:34:31 +02:00
2877d62f95 create common AddQueryString method 2025-08-15 15:15:29 +02:00
3ca148f341 feat(EConnectClient): add PostAsync method without body 2025-08-15 15:06:38 +02:00
ad9f7ef7e4 update to use get-init instead of input parameter 2025-08-15 15:03:42 +02:00
4bb6a6cf18 Add response without data 2025-08-15 14:51:29 +02:00
e85a4986e6 refactor(ClientOptions): Vereinfachung der Initialisierung von JsonSerializerOptions
- Null-Prüfung und verzögerte Initialisierung von JsonSerializerOptions entfernt
- Standard-JsonSerializerOptions mit PropertyNameCaseInsensitive = true festgelegt
- DateTimeConverter immer hinzufügen, wenn JsonSerializerDateFormat festgelegt ist
2025-08-15 14:24:05 +02:00
1ed1937c40 fix(appsettings): move JsonSerializerDateFormat 2025-08-15 14:14:47 +02:00
3082c0b77c feat(EConnectClient): add json serilization options to EConnectClient 2025-08-15 14:10:19 +02:00
6836b422a4 feta(ClientOptions): add JsonSerializerDateFormat and JsonSerializerOptions. 2025-08-15 14:03:09 +02:00
6b2c897e5b feat(DateTimeConverter): add to handle custom date time formatter http response deserilization 2025-08-15 13:49:37 +02:00
db3137ef9d feat(order): Aktualisierung von GetDocumentRequest, um mehrere OrderDocuments zurückzugeben
- Antworttyp von OrderDocument zu IEnumerable<OrderDocument> geändert
- Handler aktualisiert, um GetAsync mit IEnumerable<OrderDocument> aufzurufen
2025-08-15 13:02:11 +02:00
02b4aa342a Aktualisieren den Standardwert für apiVersion auf 1 2025-08-15 12:41:27 +02:00
57b273cde4 fix(ClientOptions): Die Eigenschaft „QueryStrings” in „DefaultQueryStrings” umbenennen. 2025-08-15 12:33:33 +02:00
aa192626c2 fix(GetExtensions): set default apiVersion to 1 2025-08-15 12:25:33 +02:00
9142b9c49a feat(OrderController): create and add GetDocument endpoint 2025-08-15 11:47:40 +02:00
f994781713 feat(client): Implementierung von GetDocumentRequestHandler mit IEConnectClient-Integration 2025-08-15 11:19:50 +02:00
65d59c6c67 feat(GetDocumentRequest): created to handle order-document get request 2025-08-15 11:06:48 +02:00
32b631a6c2 refactor(ClientOptions): add AfterHttpInit-property and bind to dependency injection 2025-08-15 10:52:39 +02:00
3eacbd89f7 refactor(DependencyInjection): AddEConnectClient in AddClientServices umbenennen 2025-08-15 10:41:26 +02:00
3af571ea37 refactor(ClientOptions): move to inf layer 2025-08-15 10:39:25 +02:00
c21e4a93ef refactor(appsettings): apiKey in QueryStrings verschieben 2025-08-15 10:24:01 +02:00
dd60555ed3 feat(ClientOptions): add QueryStrings-property.
- add logic to handle global query strings on EConnectClient
2025-08-15 10:20:33 +02:00
a7cbced3e6 refactor(ObjectExtensions): add and bind ToCamelCase-method 2025-08-15 10:07:59 +02:00
786086a260 refactor(EConnectClient): update to add null-check 2025-08-15 09:57:40 +02:00
087df71b7b refactor(EConnectClient): queryParams-Eingabe zu get-Methoden hinzufügen 2025-08-15 09:46:29 +02:00
a7a16ab281 fix(queryPrams): queryPrams in queryParams umbenennen 2025-08-15 09:40:16 +02:00
Developer 02
2d8d5442d1 bind appsettings 2025-08-14 18:57:27 +02:00
Developer 02
fe198615fc Verbinde HttpClient mit ClientOptions 2025-08-14 18:46:04 +02:00
Developer 02
8c6719f516 feat(ClientOptions): add BaseAddress and Timeout 2025-08-14 18:39:53 +02:00
Developer 02
8db4b6e15a add dependencies and configure AddEConnectClient 2025-08-14 18:33:21 +02:00
Developer 02
92910ecb19 add proxy server 2025-08-14 18:27:01 +02:00
Developer 02
ce33b50953 merge options 2025-08-14 18:20:08 +02:00
Developer 02
2e59c090a8 feat(EConnectClientOptions): created and inject 2025-08-14 17:49:04 +02:00
Developer 02
c6ec3ca054 Refactor: Verwenden Sie Lazy<HttpClient> in EConnectClient, um die Erstellung zu verzögern. 2025-08-14 17:41:39 +02:00
Developer 02
9117a23be3 refactor(econnect-client): Gespeicherten HttpClient entfernen und pro Anfrage erstellen
- Feld „_http“ durch „_httpFactory“ ersetzt, um die Erstellung von HttpClient zu verzögern
- Eigenschaft „Http“ hinzugefügt, um einen neuen Client aus der Factory abzurufen
- Alle Verwendungen aktualisiert, um die Eigenschaft „Http“ anstelle der gespeicherten Instanz zu verwenden
2025-08-14 17:27:28 +02:00
Developer 02
4b8217bb80 add ToPropertyDictionary to convert Dictionary<string, object?> to Dictionary<string, string?> 2025-08-14 17:20:59 +02:00
Developer 02
6cf47dc617 feat: AddQueryString-Erweiterungsmethoden für String-Routen hinzufügen 2025-08-14 17:05:53 +02:00
Developer 02
94c6813306 feat: Hinzufügen von ObjectExtensions mit Eigenschaft-zu-Wörterbuch und sicherer Zeichenfolgenkonvertierung 2025-08-14 16:34:56 +02:00
Developer 02
682fb772f7 feat(infrastructure): Unterstützung für generische IEConnectClient<>-Registrierung hinzugefügt 2025-08-12 20:10:18 +02:00
Developer 02
9d5bf509d5 feat(EConnectClient): created as an implementation of IEConnectClient 2025-08-12 20:08:36 +02:00
Developer 02
757ba77179 feat(IEConnectClient): create interface to handle abse http client operations 2025-08-12 19:38:22 +02:00
Developer 02
11ab4388d0 create response class to handle http result 2025-08-12 19:29:32 +02:00
Developer 02
d904fded39 refactor: rename HttpClient-related members to EConnectClient for clarity 2025-08-12 18:46:38 +02:00
Developer 02
e6a8c81a0b feat(infrastructure): add extension method to create EConnect HttpClient 2025-08-12 18:44:41 +02:00
Developer 02
4256a79122 feat(infrastructure): add extension methods for configuring and registering HttpClient
- Implemented ConfigureHttpClient extension for IServiceCollection with dynamic client name
- Added AddInfrastructureServices method to simplify HttpClient registration via configurable options
- Introduced sealed Config class for flexible client configuration
2025-08-12 18:41:07 +02:00
Developer 02
c3c6ffdf99 init Leanetec.EConnect.Infrastructure 2025-08-12 17:11:55 +02:00
Developer 02
8b248db4e2 feat(domain): neue Entität 'ProblemDetail' hinzugefügt 2025-08-12 17:02:03 +02:00
Developer 02
c5787c8736 feat(domain): neue Entität 'OrderDocument' hinzugefügt 2025-08-12 16:50:28 +02:00
32 changed files with 976 additions and 70 deletions

View File

@@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{AC628874-E
docs\econnect-api_swagger.pdf = docs\econnect-api_swagger.pdf docs\econnect-api_swagger.pdf = docs\econnect-api_swagger.pdf
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Leanetec.EConnect.Infrastructure", "src\Leanetec.EConnect.Infrastructure\Leanetec.EConnect.Infrastructure.csproj", "{88A1AA25-45F3-4082-8B5A-F95FC8A21057}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Leanetec.EConnect.Proxy", "src\Leanetec.EConnect.Proxy\Leanetec.EConnect.Proxy.csproj", "{79D9868E-6A67-45C1-BA3E-1C2E822ECC58}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -29,6 +33,14 @@ Global
{9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Release|Any CPU.Build.0 = Release|Any CPU {9242EEA9-447B-44A6-A66D-D671DD16D0BB}.Release|Any CPU.Build.0 = Release|Any CPU
{88A1AA25-45F3-4082-8B5A-F95FC8A21057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88A1AA25-45F3-4082-8B5A-F95FC8A21057}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88A1AA25-45F3-4082-8B5A-F95FC8A21057}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88A1AA25-45F3-4082-8B5A-F95FC8A21057}.Release|Any CPU.Build.0 = Release|Any CPU
{79D9868E-6A67-45C1-BA3E-1C2E822ECC58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79D9868E-6A67-45C1-BA3E-1C2E822ECC58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79D9868E-6A67-45C1-BA3E-1C2E822ECC58}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79D9868E-6A67-45C1-BA3E-1C2E822ECC58}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -36,6 +48,8 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{34DC964A-1905-7DFC-0125-D551D3EEFCDD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {34DC964A-1905-7DFC-0125-D551D3EEFCDD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{9242EEA9-447B-44A6-A66D-D671DD16D0BB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {9242EEA9-447B-44A6-A66D-D671DD16D0BB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88A1AA25-45F3-4082-8B5A-F95FC8A21057} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{79D9868E-6A67-45C1-BA3E-1C2E822ECC58} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8C81AD6F-B903-4C78-873C-38EE216EFAD5} SolutionGuid = {8C81AD6F-B903-4C78-873C-38EE216EFAD5}

View File

@@ -11,7 +11,7 @@ public static class GetExtensions
/// <param name="role">Optional role of the logged-in user.</param> /// <param name="role">Optional role of the logged-in user.</param>
/// <param name="apiVersion">Optional API version.</param> /// <param name="apiVersion">Optional API version.</param>
/// <returns>A <see cref="Task{Boolean}"/> representing whether the application is alive.</returns> /// <returns>A <see cref="Task{Boolean}"/> representing whether the application is alive.</returns>
public static Task<bool> IsAliveAsync(this IMediator mediator, Role? role = null, int? apiVersion = null) => public static Task<bool> IsAliveAsync(this IMediator mediator, Role? role = null, int apiVersion = 1) =>
mediator.Send(new GetRequest(role) mediator.Send(new GetRequest(role)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
@@ -24,7 +24,7 @@ public static class GetExtensions
/// <param name="role">Optional role of the logged-in user.</param> /// <param name="role">Optional role of the logged-in user.</param>
/// <param name="apiVersion">Optional API version.</param> /// <param name="apiVersion">Optional API version.</param>
/// <returns>True if the application is alive; otherwise, false.</returns> /// <returns>True if the application is alive; otherwise, false.</returns>
public static bool IsAlive(this IMediator mediator, Role? role = null, int? apiVersion = null) => public static bool IsAlive(this IMediator mediator, Role? role = null, int apiVersion = 1) =>
mediator.Send(new GetRequest(role) mediator.Send(new GetRequest(role)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion

View File

@@ -19,7 +19,7 @@ public static class GetExtensions
/// <returns> /// <returns>
/// A task that represents the asynchronous operation. The task result contains the matching <see cref="Article"/>, or <c>null</c> if not found. /// A task that represents the asynchronous operation. The task result contains the matching <see cref="Article"/>, or <c>null</c> if not found.
/// </returns> /// </returns>
public static async Task<Article?> GetArticleByIdAsync(this IMediator mediator, int id, int? apiVersion = null) public static async Task<Article?> GetArticleByIdAsync(this IMediator mediator, int id, int apiVersion = 1)
{ {
var articles = await mediator.Send(new GetRequest(id) { ApiVersion = apiVersion }); var articles = await mediator.Send(new GetRequest(id) { ApiVersion = apiVersion });
return articles.FirstOrDefault(); return articles.FirstOrDefault();
@@ -34,7 +34,7 @@ public static class GetExtensions
/// <returns> /// <returns>
/// The matching <see cref="Article"/>, or <c>null</c> if not found. /// The matching <see cref="Article"/>, or <c>null</c> if not found.
/// </returns> /// </returns>
public static Article? GetArticleById(this IMediator mediator, int id, int? apiVersion = null) public static Article? GetArticleById(this IMediator mediator, int id, int apiVersion = 1)
{ {
var articles = mediator.Send(new GetRequest(id) { ApiVersion = apiVersion }).GetAwaiter().GetResult(); var articles = mediator.Send(new GetRequest(id) { ApiVersion = apiVersion }).GetAwaiter().GetResult();
return articles.FirstOrDefault(); return articles.FirstOrDefault();
@@ -52,7 +52,7 @@ public static class GetExtensions
/// <returns> /// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of all available <see cref="Article"/> entities. /// A task that represents the asynchronous operation. The task result contains a collection of all available <see cref="Article"/> entities.
/// </returns> /// </returns>
public static Task<IEnumerable<Article>> GetAllArticlesAsync(this IMediator mediator, int? apiVersion = null) => mediator.Send(new GetRequest() public static Task<IEnumerable<Article>> GetAllArticlesAsync(this IMediator mediator, int apiVersion = 1) => mediator.Send(new GetRequest()
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}); });
@@ -65,7 +65,7 @@ public static class GetExtensions
/// <returns> /// <returns>
/// A collection of all available <see cref="Article"/> entities. /// A collection of all available <see cref="Article"/> entities.
/// </returns> /// </returns>
public static IEnumerable<Article> GetAllArticles(this IMediator mediator, int? apiVersion = null) => mediator.Send(new GetRequest() public static IEnumerable<Article> GetAllArticles(this IMediator mediator, int apiVersion = 1) => mediator.Send(new GetRequest()
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();

View File

@@ -15,7 +15,7 @@ public static class PostExtensions
/// <param name="article">The <see cref="Article"/> entity to be created.</param> /// <param name="article">The <see cref="Article"/> entity to be created.</param>
/// <param name="apiVersion">Optional API version to include in the request.</param> /// <param name="apiVersion">Optional API version to include in the request.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task CreateArticleAsync(this IMediator mediator, Article article, int? apiVersion = null) => mediator.Send(new PostRequest(article) public static Task CreateArticleAsync(this IMediator mediator, Article article, int apiVersion = 1) => mediator.Send(new PostRequest(article)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}); });
@@ -26,7 +26,7 @@ public static class PostExtensions
/// <param name="mediator">The <see cref="IMediator"/> instance used to send the request.</param> /// <param name="mediator">The <see cref="IMediator"/> instance used to send the request.</param>
/// <param name="article">The <see cref="Article"/> entity to be created.</param> /// <param name="article">The <see cref="Article"/> entity to be created.</param>
/// <param name="apiVersion">Optional API version to include in the request.</param> /// <param name="apiVersion">Optional API version to include in the request.</param>
public static void CreateArticle(this IMediator mediator, Article article, int? apiVersion = null) => mediator.Send(new PostRequest(article) public static void CreateArticle(this IMediator mediator, Article article, int apiVersion = 1) => mediator.Send(new PostRequest(article)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();

View File

@@ -15,7 +15,7 @@ public static class PutExtensions
/// <param name="article">The <see cref="Article"/> entity to be updated.</param> /// <param name="article">The <see cref="Article"/> entity to be updated.</param>
/// <param name="apiVersion">Optional API version to include in the request.</param> /// <param name="apiVersion">Optional API version to include in the request.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task UpdateArticleAsync(this IMediator mediator, Article article, int? apiVersion = null) => mediator.Send(new PutRequest(article) public static Task UpdateArticleAsync(this IMediator mediator, Article article, int apiVersion = 1) => mediator.Send(new PutRequest(article)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}); });
@@ -26,7 +26,7 @@ public static class PutExtensions
/// <param name="mediator">The <see cref="IMediator"/> instance used to send the request.</param> /// <param name="mediator">The <see cref="IMediator"/> instance used to send the request.</param>
/// <param name="article">The <see cref="Article"/> entity to be updated.</param> /// <param name="article">The <see cref="Article"/> entity to be updated.</param>
/// <param name="apiVersion">Optional API version to include in the request.</param> /// <param name="apiVersion">Optional API version to include in the request.</param>
public static void UpdateArticle(this IMediator mediator, Article article, int? apiVersion = null) => mediator.Send(new PutRequest(article) public static void UpdateArticle(this IMediator mediator, Article article, int apiVersion = 1) => mediator.Send(new PutRequest(article)
{ {
ApiVersion = apiVersion ApiVersion = apiVersion
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();

View File

@@ -1,49 +0,0 @@
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace Leanetec.EConnect.Client;
/// <summary>
/// Provides a lazily initialized <see cref="IServiceProvider"/> for accessing the EConnect client services,
/// including an <see cref="IMediator"/> instance for sending and publishing messages.
/// </summary>
public static class Client
{
private static Action<ClientOptions>? _options = null;
/// <summary>
/// Gets or sets the configuration options for the EConnect client.
/// Can only be set once; subsequent attempts to set will result in an exception.
/// </summary>
public static Action<ClientOptions> Options
{
get => _options ?? throw new InvalidOperationException("EConnect Client options have not been configured. Please set the 'Client.Options' property before accessing client services.");
set
{
if (_options is null)
_options = value;
else
throw new InvalidOperationException("EConnect Client options have already been configured. Reassignment is not allowed.");
}
}
/// <summary>
/// Lazily initializes the <see cref="IServiceProvider"/> that registers and builds the EConnect client services.
/// </summary>
private static readonly Lazy<IServiceProvider> _serviceProvider = new(() =>
{
var services = new ServiceCollection();
services.AddEConnectClient(Options);
return services.BuildServiceProvider();
});
/// <summary>
/// Gets the initialized <see cref="IServiceProvider"/> that provides access to registered EConnect client services.
/// </summary>
public static IServiceProvider Provider => _serviceProvider.Value;
/// <summary>
/// Gets the <see cref="IMediator"/> instance used for sending commands, queries, and publishing events within the EConnect client.
/// </summary>
public static IMediator Mediator => Provider.GetRequiredService<IMediator>();
}

View File

@@ -1,6 +0,0 @@
namespace Leanetec.EConnect.Client;
public class ClientOptions
{
public int ApiVersion { get; set; } = 1;
}

View File

@@ -4,9 +4,8 @@ namespace Leanetec.EConnect.Client;
public static class DependencyInjection public static class DependencyInjection
{ {
public static IServiceCollection AddEConnectClient(this IServiceCollection services, Action<ClientOptions> clientOptions) public static IServiceCollection AddClientServices(this IServiceCollection services)
{ {
services.Configure(clientOptions);
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
return services; return services;
} }

View File

@@ -4,7 +4,7 @@ namespace Leanetec.EConnect.Client.Dto;
public record HttpRequestBase public record HttpRequestBase
{ {
public int? ApiVersion { get; set; } public int ApiVersion { get; set; } = 1;
} }
public record HttpRequest : HttpRequestBase, IRequest public record HttpRequest : HttpRequestBase, IRequest

View File

@@ -0,0 +1,10 @@
using Leanetec.EConnect.Domain.Entities;
namespace Leanetec.EConnect.Client.Interface;
public interface IEConnectClient<TError> where TError : class
{
public Task<Response<TData, TError>> GetAsync<TData>(string? route = null, object? queryParams = null, CancellationToken cancel = default) where TData : class;
Task<Response<TError>> PostAsync(Stream stream, string fileName, string? route = null, object? queryParams = null, CancellationToken cancel = default);
}

View File

@@ -1,15 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="13.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="MediatR" Version="12.5.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="MediatR" Version="13.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="MediatR" Version="13.0.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Leanetec.EConnect.Domain\Leanetec.EConnect.Domain.csproj" /> <ProjectReference Include="..\Leanetec.EConnect.Domain\Leanetec.EConnect.Domain.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,30 @@
using Leanetec.EConnect.Client.Dto;
using Leanetec.EConnect.Client.Interface;
using Leanetec.EConnect.Domain.Entities;
using MediatR;
namespace Leanetec.EConnect.Client.Order;
//TODO: EConnectClient.GetListAsAsyncEnumerable
public record GetDocumentRequest(string TenantId, int OrderId) : HttpRequest<Response<IEnumerable<OrderDocument>, ProblemDetail>>
{
}
public class GetDocumentRequestHandler : IRequestHandler<GetDocumentRequest, Response<IEnumerable<OrderDocument>, ProblemDetail>>
{
private readonly IEConnectClient<ProblemDetail> _client;
public GetDocumentRequestHandler(IEConnectClient<ProblemDetail> client)
{
_client = client;
}
public async Task<Response<IEnumerable<OrderDocument>, ProblemDetail>> Handle(GetDocumentRequest request, CancellationToken cancel)
{
return await _client.GetAsync<IEnumerable<OrderDocument>>(
$"api/public/v{request.ApiVersion}/econnect/order/document/list",
new { request.TenantId, request.OrderId },
cancel
);
}
}

View File

@@ -0,0 +1,37 @@
using Leanetec.EConnect.Client.Dto;
using Leanetec.EConnect.Client.Interface;
using Leanetec.EConnect.Domain.Entities;
using MediatR;
namespace Leanetec.EConnect.Client.Order;
public record PostDocumentRequest(string TenantId, int OrderId) : HttpRequest<Response<ProblemDetail>>
{
public UploadDocumentRequest ToUploadDocument(Stream stream, string fileName)
{
return new UploadDocumentRequest(this, stream, fileName);
}
}
public record UploadDocumentRequest(PostDocumentRequest Original, Stream Stream, string FileName) : PostDocumentRequest(Original);
public class PostDocumentRequestHandler : IRequestHandler<UploadDocumentRequest, Response<ProblemDetail>>
{
private readonly IEConnectClient<ProblemDetail> _client;
public PostDocumentRequestHandler(IEConnectClient<ProblemDetail> client)
{
_client = client;
}
public async Task<Response<ProblemDetail>> Handle(UploadDocumentRequest request, CancellationToken cancel)
{
return await _client.PostAsync(
request.Stream,
request.FileName,
$"api/public/v{request.ApiVersion}/econnect/order/document",
new { request.TenantId, request.OrderId },
cancel
);
}
}

View File

@@ -0,0 +1,39 @@
namespace Leanetec.EConnect.Domain.Entities;
public class OrderDocument
{
/// <summary>
/// The unique internal id of the file
/// </summary>
public int Id { get; init; }
/// <summary>
/// The name of the file
/// </summary>
public string? FileName { get; init; }
/// <summary>
/// The size of the file in bytes
/// </summary>
public int FileSizeInBytes { get; init; }
/// <summary>
/// The internal unique id of the folder containing this file
/// </summary>
public int ParentFolderId { get; init; }
/// <summary>
/// The string representation of the timstamp when this file has been created (uploaded)
/// </summary>
public DateTime? CreatedOn { get; init; }
/// <summary>
/// The string representation of the timstamp when this file has been updated
/// </summary>
public DateTime? LastUpdateOn { get; init; }
/// <summary>
/// The MIME type of the file
/// </summary>
public string? FileMimeType { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Leanetec.EConnect.Domain.Entities;
public class ProblemDetail
{
public string? Type { get; init; }
public string? Title { get; init; }
public int? Status { get; init; }
public string? Detail { get; init; }
public string? Instance { get; init; }
}

View File

@@ -0,0 +1,25 @@
using System.Net;
namespace Leanetec.EConnect.Domain.Entities;
public record Response<TError>() where TError : class
{
public bool Ok { get; init; }
private HttpStatusCode? _statusCode;
public HttpStatusCode StatusCode
{
get => _statusCode ?? (Ok ? HttpStatusCode.OK : HttpStatusCode.InternalServerError);
init => _statusCode = value;
}
public int StatusCodeInt => (int)StatusCode;
public TError? Error { get; init; }
}
public record Response<TData, TError> : Response<TError> where TData : class where TError : class
{
public TData? Data { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace Leanetec.EConnect.Infrastructure;
public static class ClientExtensions
{
public static HttpClient CreateEConnectClient(this IHttpClientFactory factory) => factory.CreateClient(DependencyInjection.EConnectClientName);
}

View File

@@ -0,0 +1,34 @@
using System.Text.Json;
namespace Leanetec.EConnect.Infrastructure;
public class ClientOptions
{
public string? BaseAddress { get; set; }
public TimeSpan? Timeout { get; set; }
public Dictionary<string, string?>? DefaultQueryStrings { get; set; }
public Action<HttpClient>? AfterHttpInit { get; set; }
private string? _jsonSerializerDateFormat = null;
public string? JsonSerializerDateFormat
{
get => _jsonSerializerDateFormat;
set
{
_jsonSerializerDateFormat = value;
if (value is not null)
{
JsonSerializerOptions.Converters.Add(new DateTimeConverter(value));
}
}
}
public JsonSerializerOptions JsonSerializerOptions { get; set; } = new()
{
PropertyNameCaseInsensitive = true
};
}

View File

@@ -0,0 +1,26 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Leanetec.EConnect.Infrastructure;
public class DateTimeConverter : JsonConverter<DateTime>
{
private readonly string _format;
public DateTimeConverter(string format)
{
_format = format;
}
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return DateTime.ParseExact(value!, _format, CultureInfo.InvariantCulture);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(_format));
}
}

View File

@@ -0,0 +1,58 @@
using Leanetec.EConnect.Client.Interface;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Leanetec.EConnect.Infrastructure;
public static class DependencyInjection
{
internal static readonly string EConnectClientName = Guid.NewGuid().ToString();
internal static IServiceCollection AddEConnectHttpClient(this IServiceCollection services)
{
services.AddHttpClient(EConnectClientName, (provider, client) => {
var opt = provider.GetRequiredService<IOptions<ClientOptions>>().Value;
// add common parameters
if (opt.BaseAddress is string baseAddress)
client.BaseAddress = new Uri(baseAddress);
if (opt.Timeout is TimeSpan timeout)
client.Timeout = timeout;
opt.AfterHttpInit?.Invoke(client);
});
return services;
}
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, Action<Config>? options = null)
{
Config config = new(services);
options?.Invoke(config);
services.AddEConnectHttpClient();
services.AddScoped(typeof(IEConnectClient<>), typeof(EConnectClient<>));
return services;
}
public sealed class Config
{
private readonly IServiceCollection _services;
internal Config(IServiceCollection services)
{
_services = services;
}
public void ConfigureClient(IConfiguration config)
{
_services.Configure<ClientOptions>(config);
}
public void ConfigureClient(Action<ClientOptions> options)
{
_services.Configure(options);
}
}
}

View File

@@ -0,0 +1,118 @@
using Leanetec.EConnect.Client.Interface;
using Leanetec.EConnect.Domain.Entities;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace Leanetec.EConnect.Infrastructure;
public class EConnectClient<TError> : IEConnectClient<TError> where TError : class
{
private readonly ClientOptions _options;
private readonly Lazy<HttpClient> LazyHttp;
private HttpClient Http => LazyHttp.Value;
private readonly ILogger<EConnectClient<TError>>? _logger;
public EConnectClient(IOptions<ClientOptions> options, IHttpClientFactory httpFactory, ILogger<EConnectClient<TError>>? logger = null)
{
_options = options.Value;
LazyHttp = new Lazy<HttpClient>(httpFactory.CreateEConnectClient);
_logger = logger;
}
private string? AddQueryString(string? route = null, object? queryParams = null)
{
// add global query strings
if (_options.DefaultQueryStrings is not null)
route = route.AddQueryString(_options.DefaultQueryStrings);
// add query strings
if (queryParams is not null)
route = route.AddQueryString(queryParams.ToPropertyDictionary());
return route;
}
public async Task<Response<TData, TError>> GetAsync<TData>(string? route = null, object? queryParams = null, CancellationToken cancel = default)
where TData : class
{
route = AddQueryString(route, queryParams);
var res = await Http.GetAsync(route, cancel);
_logger?.LogCurl(Http, HttpMethod.Get, route);
return res.IsSuccessStatusCode
? new()
{
Ok = true,
StatusCode = res.StatusCode,
Data = (res.Content.Headers.ContentLength > 0)
? await res.Content.ReadFromJsonAsync<TData>(_options.JsonSerializerOptions, cancel)
: null
}
: new()
{
Ok = false,
StatusCode = res.StatusCode,
Error = (res.Content.Headers.ContentLength > 0)
? await res.Content.ReadFromJsonAsync<TError>(_options.JsonSerializerOptions, cancel)
: null
};
}
public async Task<Response<TError>> PostAsync(Stream stream, string fileName, string? route = null, object? queryParams = null, CancellationToken cancel = default)
{
route = AddQueryString(route, queryParams);
// create message and add accept header
using var message = new HttpRequestMessage(HttpMethod.Post, route);
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
// add file type
var fileContent = new StreamContent(stream);
var mimeType = GetMimeType(fileName);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
// Create multipart form data form
using var form = new MultipartFormDataContent
{
{ fileContent, "file", fileName }
};
message.Content = form;
var res = await Http.SendAsync(message, cancel);
_logger?.LogCurl(Http, message);
return res.IsSuccessStatusCode
? new()
{
Ok = true,
StatusCode = res.StatusCode
}
: new()
{
Ok = false,
StatusCode = res.StatusCode,
Error = (res.Content.Headers.ContentLength > 0)
? await res.Content.ReadFromJsonAsync<TError>(_options.JsonSerializerOptions, cancel)
: null
};
}
private static string GetMimeType(string fileName)
{
var provider = new FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(fileName, out var contentType))
{
contentType = "application/octet-stream"; // fallback
}
return contentType;
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HttpClientToCurl" Version="2.0.6" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.19" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.19" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Leanetec.EConnect.Client\Leanetec.EConnect.Client.csproj" />
<ProjectReference Include="..\Leanetec.EConnect.Domain\Leanetec.EConnect.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,100 @@
using HttpClientToCurl;
using HttpClientToCurl.Config;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
namespace Leanetec.EConnect.Infrastructure;
public static class LogExtensions
{
public static void LogCurl(
this ILogger logger,
HttpClient client,
HttpRequestMessage request,
Action<StringConfig>? config = null,
int maxLength = 1000,
LogLevel logLevel = LogLevel.Information)
{
try
{
var curl = client.GenerateCurlInString(
request,
config
).Truncate(maxLength);
logger?.Log(logLevel, "{curl}", curl);
}
catch(Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while logging the HTTP request as cURL.");
}
}
public static void LogCurl(
this ILogger logger,
HttpClient client,
HttpMethod method,
string? uri = null,
HttpRequestHeaders? headers = null,
HttpContent? content = null,
Action<StringConfig>? config = null,
int maxLength = 1000,
LogLevel logLevel = LogLevel.Information)
{
try
{
var curl = client.GenerateCurlInString(
method,
uri ?? "",
headers,
content,
config
).Truncate(maxLength);
logger?.Log(logLevel, "{curl}", curl);
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while logging the HTTP request as cURL.");
}
}
public static void LogCurl(
this ILogger logger,
HttpClient client,
HttpMethod method,
Uri uri,
HttpRequestHeaders? headers = null,
HttpContent? content = null,
Action<StringConfig>? config = null,
int maxLength = 1000,
LogLevel logLevel = LogLevel.Information)
{
try
{
var curl = client.GenerateCurlInString(
method,
uri,
headers,
content,
config
).Truncate(maxLength);
logger?.Log(logLevel, "{curl}", curl);
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while logging the HTTP request as cURL.");
}
}
/// <summary>
/// Truncates the string to the specified max length and appends a suffix if truncated.
/// </summary>
public static string Truncate(this string? value, int maxLength, string suffix = "...[truncated]")
{
if (string.IsNullOrEmpty(value) || maxLength <= 0)
return string.Empty;
return value.Length <= maxLength
? value
: value[..maxLength] + suffix;
}
}

View File

@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.WebUtilities;
using System.Reflection;
namespace Leanetec.EConnect.Infrastructure;
public static class ObjectExtensions
{
public static Dictionary<string, string?> ToPropertyDictionary(this object obj)
{
return obj
.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(
prop => prop.Name.ToCamelCase(),
prop => prop.GetValue(obj).ToSafeString()
);
}
public static Dictionary<string, string?> ToPropertyDictionary(this Dictionary<string, object?> obj)
{
return obj
.ToDictionary(
prop => prop.Key.ToCamelCase(),
prop => prop.Value?.ToString()
);
}
public static string? ToSafeString(this object? obj)
=> obj is bool b
? (b ? "true" : "false")
: obj?.ToString();
public static string? AddQueryString(this string? route, Dictionary<string, string?> queryParams)
{
if (queryParams.Count > 0)
route = QueryHelpers.AddQueryString(route ?? string.Empty, queryParams);
return route;
}
public static string? AddQueryString(this string? route, object queryParams) => route.AddQueryString(queryParams.ToPropertyDictionary());
public static string ToCamelCase(this string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
if (char.IsLower(input[0]))
return input;
return char.ToLowerInvariant(input[0]) + input[1..];
}
}

View File

@@ -0,0 +1,43 @@
using Leanetec.EConnect.Client.Order;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace Leanetec.EConnect.Proxy.Controllers;
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("document")]
public async Task<IActionResult> GetDocument([FromQuery] GetDocumentRequest request, CancellationToken cancel)
{
var res = await _mediator.Send(request, cancel);
if(res.Ok)
return res.Data is null || !res.Data.Any()
? NotFound()
: Ok(res.Data);
else
return StatusCode(res.StatusCodeInt, res?.Error);
}
[HttpPost("document")]
public async Task<IActionResult> PostDocument(IFormFile file, [FromQuery] PostDocumentRequest request, CancellationToken cancel)
{
using var stream = file.OpenReadStream();
var uploadRequest = request.ToUploadDocument(stream, file.FileName);
var res = await _mediator.Send(uploadRequest, cancel);
return res.Ok
? StatusCode(res.StatusCodeInt)
: StatusCode(res.StatusCodeInt, res?.Error);
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>Leanetec.EConnect.Proxy</PackageId>
<Version>1.0.1</Version>
<Company>Digital Data GmbH</Company>
<Product>Leanetec.EConnect.Proxy</Product>
<Title>Leanetec.EConnect.Proxy</Title>
<AssemblyVersion>1.0.1</AssemblyVersion>
<FileVersion>1.0.1</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Leanetec.EConnect.Client\Leanetec.EConnect.Client.csproj" />
<ProjectReference Include="..\Leanetec.EConnect.Infrastructure\Leanetec.EConnect.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,84 @@
using DigitalData.Core.Exceptions;
using System.Net;
using System.Text.Json;
namespace Leanetec.EConnect.Proxy.Middleware;
//TODO: Fix and use DigitalData.Core.Exceptions.Middleware
/// <summary>
/// Middleware for handling exceptions globally in the application.
/// Captures exceptions thrown during the request pipeline execution,
/// logs them, and returns an appropriate HTTP response with a JSON error message.
/// </summary>
[Obsolete("Use DigitalData.Core.Exceptions.Middleware")]
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the request pipeline.</param>
/// <param name="logger">The logger instance for logging exceptions.</param>
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware to handle the HTTP request.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context); // Continue down the pipeline
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, _logger);
}
}
/// <summary>
/// Handles exceptions by logging them and writing an appropriate JSON response.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <param name="exception">The exception that occurred.</param>
/// <param name="logger">The logger instance for logging the exception.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
{
context.Response.ContentType = "application/json";
string message;
switch (exception)
{
case BadRequestException badRequestEx:
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
message = badRequestEx.Message;
break;
case NotFoundException notFoundEx:
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
message = notFoundEx.Message;
break;
default:
logger.LogError(exception, "Unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
message = "An unexpected error occurred.";
break;
}
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
message
}));
}
}

View File

@@ -0,0 +1,65 @@
using Leanetec.EConnect.Client;
using Leanetec.EConnect.Infrastructure;
using Leanetec.EConnect.Proxy.Middleware;
using NLog;
using NLog.Web;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
try
{
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
if (!builder.Environment.IsDevelopment())
{
builder.Logging.ClearProviders();
builder.Host.UseNLog();
}
// Add services to the container.
var clientOpt = config.GetSection("EConnect");
builder.Services.AddClientServices().AddInfrastructureServices(opt => opt.ConfigureClient(clientOpt));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseMiddleware<ExceptionHandlingMiddleware>();
bool useSwagger = config.GetValue<bool>("UseSwagger");
if(useSwagger)
app.Services.GetRequiredService<ILogger<Program>>()
.LogWarning("Swagger UI is enabled. Using Swagger in a production environment may expose sensitive API information and pose security risks.");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || useSwagger)
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
// NLog: catch setup errors
logger.Error(ex, "Application stopped because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
LogManager.Shutdown();
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>79d9868e-6a67-45c1-ba3e-1c2e822ecc58</ProjectGuid>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\EConnect\Proxy\$(Version)\EConnect.Proxy.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>EConnect.Proxy</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:25272",
"sslPort": 44340
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5254",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7067;http://localhost:5254",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,68 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"UseSwagger": true,
"AllowedHosts": "*",
"EConnect": {
"BaseAddress": "https://portal.demoportal01.leanetec.com",
"DefaultQueryStrings": {
"apiKey": "HGpGp3vk3MsKgSe0tKVZ1ZRCuq6bFoJ1"
},
"JsonSerializerDateFormat": "yyyy-MM-dd HH:mm"
},
"NLog": {
"throwConfigExceptions": true,
"variables": {
"logDirectory": "E:\\LogFiles\\Digital Data\\EConnect-Proxy",
"logFileNamePrefix": "${shortdate}-ECM.EConnect-Proxy.Web"
},
"targets": {
"infoLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
"maxArchiveDays": 30
},
"warningLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Warning.log",
"maxArchiveDays": 30
},
"errorLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
"maxArchiveDays": 30
},
"criticalLogs": {
"type": "File",
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
"maxArchiveDays": 30
}
},
"rules": [
{
"logger": "*",
"level": "Info",
"writeTo": "infoLogs"
},
{
"logger": "*",
"level": "Warn",
"writeTo": "warningLogs"
},
{
"logger": "*",
"level": "Error",
"writeTo": "errorLogs"
},
{
"logger": "*",
"level": "Fatal",
"writeTo": "criticalLogs"
}
]
}
}