Compare commits

...

24 Commits

Author SHA1 Message Date
cd221e710e Add new Client Dependency Injection project
A new project, `DigitalData.MessagingService.Client.DependencyInjection`, has been added to the solution.

- Updated `DigitalData.MessagingService.sln` to include the new project with its build configurations and nested project structure.
- Created `DigitalData.MessagingService.Client.DependencyInjection.csproj` targeting `net462`, `net480`, and `net8.0`.
- Added project metadata such as `PackageId`, `Authors`, `Version`, and more.
- Included a dependency on `RabbitMQ.Client` (v7.2.1).
- Configured nullable reference types and set the language version to `latest`.
- Added `icon.png` for NuGet packaging and enabled XML documentation generation.
2026-07-28 10:30:18 +02:00
78c82bf129 Refactor solution structure and add RabbitMQ config
Reorganized the solution structure to align with a layered architecture:
- Replaced `src` folder with `core`, `infrastructure`, and `presentation`.
- Moved projects to their respective folders.
- Added `DigitalData.MessagingService.Publisher.Abstraction` project.
- Removed `DigitalData.MessagingService.Client` project.

Updated project configurations and nesting in the solution file.

Added `appsettings.Secrets.json` with RabbitMQ and email account settings:
- RabbitMQ configuration includes hostname, port, credentials, and queue/exchange details.
- Email configuration includes SMTP server details and credentials.
2026-07-28 10:26:15 +02:00
2ad2dc6b4d Remove unused projects and simplify solution structure
The following projects were removed:
- `DigitalData.MessagingService.Client.Infrastructure`
- `DigitalData.MessagingService.Client`
- `DigitalData.MessagingService.Publisher.Abstraction`

The solution file (`DigitalData.MessagingService.sln`) was updated to remove references to these projects, including solution configuration platforms and nested project sections.

Additionally, the `DigitalData.MessagingService.Application.csproj` file was updated to remove a `ProjectReference` to `DigitalData.MessagingService.Publisher.Abstraction.csproj`.

These changes streamline the solution by removing unused or redundant components and reducing dependencies.
2026-07-28 10:21:07 +02:00
3149bb1cf9 Add Publisher.Abstraction project and refactor namespaces
Introduced a new project, `DigitalData.MessagingService.Publisher.Abstraction`, to encapsulate the `OutgoingEmailEvent` class and `IOutgoingEmailPublisher` interface. The project targets multiple frameworks (`net462`, `net480`, `net8.0`) and enables nullable reference types and the latest C# language version.

Moved `OutgoingEmailEvent` and `IOutgoingEmailPublisher` to the new project, updating their namespaces and replacing `required` properties with mutable ones in `OutgoingEmailEvent`. Updated all dependent files to reference the new namespace.

Updated the solution file to include the new project and adjusted build configurations. Cleaned up unused `using` directives and removed redundant `LangVersion` property in the new project file.
2026-07-28 10:18:54 +02:00
0b3ff7cae9 Add new Client.Infrastructure project to solution
A new project, `DigitalData.MessagingService.Client.Infrastructure`, has been added to the solution. This project targets `net462` and `net8.0`, with Implicit Usings, Nullable reference types, and the latest C# language version enabled. It includes dependencies on `Microsoft.Extensions.Logging.Abstractions`, `RabbitMQ.Client`, and `Microsoft.Extensions.Options.ConfigurationExtensions`.

The solution file has been updated to include the new project, along with its build configurations (Debug and Release for Any CPU). The project hierarchy has also been updated to reflect the addition of the new project and the reassignment of the `DigitalData.MessagingService.Client` project to a different parent group.
2026-07-28 09:40:37 +02:00
977a20fd97 Refactor RabbitMQ DI into extension method
Refactored the registration of `RabbitMqConnectionFactory` and
RabbitMQ configuration into a new `AddRabbitMqConnectionFactory`
extension method for improved modularity and reusability.

- Removed direct calls to `AddSingleton<RabbitMqConnectionFactory>`
  and `Configure<RabbitMqConfiguration>` from the main
  `DependencyInjection` class.
- Added a new static `DependencyInjection` class under the
  `DigitalData.MessagingService.RabbitMQ` namespace.
- The new `AddRabbitMqConnectionFactory` method encapsulates
  RabbitMQ DI logic and accepts `IServiceCollection` and
  `IConfiguration` as parameters.
- Updated `DependencyInjection.cs` to use the new extension method.
2026-07-27 17:10:45 +02:00
56ac720615 Add net480 target, enable nullable, and set LangVersion
Updated the `<TargetFrameworks>` property to include `net480`
to support .NET Framework 4.8. Added `<LangVersion>` set to
`latest` to use the latest C# language features. Enabled
nullable reference types by adding `<Nullable>` set to
`enable` for improved null safety.
2026-07-27 17:06:50 +02:00
becb608331 Make OutgoingEmailConsumer more robust and maintainable
- Added null-safety checks (`?.`) for `ILogger` usage in `OutgoingEmailConsumer`.
- Removed unused `AsyncEventingBasicConsumer` and `_lazyInit` fields.
- Updated `DisposeAsync` to check `_lazyChannel.IsValueCreated` before accessing it.
- Added infinite delay in `AsyncInitWorker` to keep the background service active until cancellation.
- Improved overall maintainability and reduced potential runtime issues.
2026-07-27 16:55:31 +02:00
37009b8e1e Update target frameworks and enable latest C# features
Updated the project to target .NET 8.0 alongside .NET Framework 4.6.2.
Added `<LangVersion>` property set to `latest` to enable the use of
the latest C# language features. Existing `<ImplicitUsings>` and
`<Nullable>` properties remain unchanged. No changes were made to
the `MediatR` package reference.
2026-07-27 16:45:03 +02:00
b2d478c335 Refactor OutgoingEmailConsumer for better initialization
Refactored the `OutgoingEmailConsumer` class to improve maintainability, readability, and robustness. Changed the class to explicitly inherit from `IAsyncDisposable` and introduced lazy initialization for RabbitMQ connections and consumers via `_lazyInit`.

Enhanced error handling in `consumer.ReceivedAsync` by adding detailed logging, `BasicNack` for invalid messages, and placeholders for error reporting strategies. Improved logging for consumer startup and added safeguards against multiple initializations.

Removed outdated comments, updated documentation, and ensured proper resource cleanup in `DisposeAsync`.
2026-07-27 16:32:34 +02:00
bf06b31656 Support multi-targeting and reformat exception classes
Updated the project file to support multi-targeting for both
.NET Framework 4.6.2 and .NET 8.0 by replacing `<TargetFramework>`
with `<TargetFrameworks>`.

Reformatted `AuthenticationFailedException` and
`NotFoundException` classes to use braces `{ }` for namespaces
and constructors for consistency. No functional changes were made
to the exception classes.
2026-07-27 15:40:52 +02:00
77e7c796fa Refactor domain model and exception handling
Significantly restructured the `DigitalData.MessagingService.Domain` project by removing unused domain-specific classes, enums, and value objects. Key changes include:

- Updated `ExceptionHandlingMiddleware` to handle `FluentValidation.ValidationException` with formatted validation errors mapped to `HttpStatusCode.BadRequest`.
- Removed foundational domain classes such as `BaseEntity`, `ValueObject`, and `IAggregateRoot`.
- Deleted enums (`AttachmentStatus`, `AuthenticationType`, `EmailStatus`, `ErrorCode`, `ProcessType`) and domain exceptions (`DomainException`, `AttachmentProcessingException`, `DmsNotAvailableException`, `InvalidPdfException`, `ValidationException`).
- Removed value objects (`EmailAddress`, `MessageId`) and the `MessageIdGenerator` service.
- Cleaned up the project structure by removing the `Events` folder reference.

These changes simplify the domain model, reduce unused code, and align the project with updated architectural goals.
2026-07-27 15:37:34 +02:00
2d7af80cd3 Refactor RabbitMQ integration for lazy initialization
Refactored `OutgoingEmailConsumer` and `OutgoingEmailPublisher` to use `Lazy<Task<IChannel>>` for channel initialization, ensuring channels are created only when needed. Simplified initialization and disposal logic by centralizing channel management.

Replaced `CancellationTokenSource` in both classes with the `CancellationToken` provided by `RabbitMqConnectionFactory`, centralizing token management. Updated methods to use the new lazy initialization pattern.

Removed `RabbitMqConnectionFactory.InitAsync` and introduced `CreateChannelAsync` and `CreateConsumerAsync` methods for simplified channel and consumer creation. Managed cancellation tokens internally with a `CancellationTokenSource`.

Simplified `AsyncInitWorker` by removing dependencies on `RabbitMqConnectionFactory` and `OutgoingEmailPublisher`. Removed redundant initialization logic.

Cleaned up unused imports, improved logging consistency, and enhanced code readability and maintainability.
2026-07-27 15:24:26 +02:00
a2373f242a Refactor RabbitMQ connection handling logic
Updated `OutgoingEmailConsumer` and `OutgoingEmailPublisher` to use `GetDefaultConnectionAsync` instead of `GetConnectionAsync` for initializing RabbitMQ connections.

Renamed `GetConnectionAsync` to `GetDefaultConnectionAsync` in `RabbitMqConnectionFactory` to improve clarity and align with naming conventions. Updated `InitAsync` in `RabbitMqConnectionFactory` to use the renamed method.

These changes improve consistency, maintainability, and clarity in RabbitMQ connection management.
2026-07-27 13:05:17 +02:00
3611d527d4 Refactor RabbitMQ functionality to new project
Moved RabbitMQ-related functionality from the
`DigitalData.MessagingService.Infrastructure` project to a new
dedicated project/namespace `DigitalData.MessagingService.RabbitMQ`.

- Updated `DependencyInjection.cs` to use the new namespace.
- Added a project reference to `RabbitMQ.csproj` in the
  `Infrastructure.csproj` file.
- Removed `RabbitMqConfiguration.cs` and `RabbitMqConnectionFactory.cs`
  from the `Infrastructure` project.
- Updated namespaces in `OutgoingEmailConsumer.cs`,
  `OutgoingEmailPublisher.cs`, and `AsyncInitWorker.cs` to use
  `DigitalData.MessagingService.RabbitMQ`.

This refactor improves modularity, maintainability, and separation
of concerns by isolating RabbitMQ functionality in its own project.
2026-07-27 13:03:39 +02:00
885365df76 Add RabbitMQ support with configuration and connection
Added support for RabbitMQ integration:
- Updated project to target `net462` and `net8.0`.
- Added NuGet dependencies: `RabbitMQ.Client`, `Microsoft.Extensions.Logging.Abstractions`, and `Microsoft.Extensions.Options.ConfigurationExtensions`.
- Introduced `RabbitMqConfiguration` class for managing RabbitMQ settings.
- Implemented `RabbitMqConnectionFactory` for creating and managing RabbitMQ connections with lazy initialization, async disposal, and logging support.
2026-07-27 12:58:49 +02:00
94d1b73c4a Add NuGet metadata and package icon to project
Updated `DigitalData.MessagingService.Client.csproj`:
- Added NuGet metadata (PackageId, Authors, Company, etc.).
- Included `icon.png` as the package icon.
- Configured multi-targeting for `net462` and `net8.0`.
- Added XML documentation file generation.

Added `icon.png` binary file to the project.
2026-07-27 10:38:34 +02:00
84c72af993 Refactor solution structure and add RabbitMQ project
Simplified `DigitalData.MessagingService.Client.csproj` by removing metadata properties and adding a `RabbitMQ.Client` package reference.

Added a new project `DigitalData.MessagingService.RabbitMQ` targeting `net462` and `net8.0` with a `RabbitMQ.Client` package reference.

Updated `DigitalData.MessagingService.sln` to reflect the new project structure, including new paths, GUIDs, and solution configuration mappings.
2026-07-27 10:32:52 +02:00
0eda732d49 Remove MailKit dependency from project
The `MailKit` package reference (version 4.17.0) was removed from the `DigitalData.MessagingService.Infrastructure.csproj` file. This change suggests that the functionality provided by `MailKit` is no longer required or has been replaced by an alternative solution.
2026-07-27 10:12:19 +02:00
42d35d9a01 Add new MessagingService.Client project to solution
A new project `DigitalData.MessagingService.Client` has been added, targeting `net462` and `net8.0`. The project includes metadata for packaging and distribution, such as `PackageId`, `Authors`, `Version`, and more.

The solution file `DigitalData.MessagingService.sln` has been updated to include the new project, with build configurations for `Debug|Any CPU` and `Release|Any CPU`. The project is nested under the `infrastructure` folder in the solution structure.
2026-07-27 10:10:49 +02:00
23d52b9427 Add "presentation", "core", and "infrastructure" projects
Added three new projects ("presentation", "core", and
"infrastructure") to the solution file with unique GUIDs.
Updated the `GlobalSection(NestedProjects)` to reflect the
new project structure, nesting the new projects under the
parent project with GUID `{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}`.
Removed outdated nested project mappings and replaced them
with mappings for the new projects. Updated the solution
file to ensure proper integration of the new projects.
2026-07-27 10:06:20 +02:00
370872e126 Remove MimeKit package reference
The `MimeKit` package reference (version `4.17.0`) has been removed from the `DigitalData.MessagingService.Application.csproj` file. This change indicates that the project no longer relies on the `MimeKit` library for its functionality.
2026-07-27 10:04:24 +02:00
b6470fce5a Refactor: Remove IOutgoingEmailConsumer interface
Simplified the codebase by removing the `IOutgoingEmailConsumer` interface and directly using the `OutgoingEmailConsumer` class.

- Removed `IOutgoingEmailConsumer` from the application.
- Updated `DependencyInjection` to register `OutgoingEmailConsumer` directly.
- Modified `OutgoingEmailConsumer` to no longer implement the removed interface.
- Updated `AsyncInitWorker` to depend directly on `OutgoingEmailConsumer`.
- Simplified initialization logic for email consumer and publisher.

These changes eliminate an unnecessary abstraction layer, making the code easier to maintain while preserving functionality.
2026-07-27 10:04:05 +02:00
1617d4ab43 Refactor RabbitMQ email queue into publisher/consumer
Refactored the RabbitMQ-based email queue system by splitting
`OutgoingEmailQueue` into `OutgoingEmailPublisher` and
`OutgoingEmailConsumer` to separate publishing and consuming
responsibilities.

- Introduced `IOutgoingEmailConsumer` and renamed
  `IOutgoingEmailQueue` to `IOutgoingEmailPublisher` for clarity.
- Updated `SendEmailCommandHandler` to use the new publisher
  abstraction.
- Added `RabbitMqConnectionFactory` to centralize RabbitMQ
  connection management.
- Updated dependency injection to register new services.
- Simplified RabbitMQ initialization logic by delegating it to
  `RabbitMqConnectionFactory`.
- Enhanced logging for better observability.
- Improved modularity and maintainability by separating concerns
  between message publishing and consuming.
2026-07-24 19:01:42 +02:00
65 changed files with 650 additions and 762 deletions

View File

@@ -5,14 +5,6 @@ VisualStudioVersion = 17.14.36717.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.API", "src\DigitalData.MessagingService.API\DigitalData.MessagingService.API.csproj", "{456817AB-67A3-49DD-9EC5-1253A727B958}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Infrastructure", "src\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj", "{D2C32417-FE92-4063-97A9-3DA8D811E561}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Domain", "src\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj", "{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Application", "src\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj", "{1874A827-C6A5-EB5E-0FE9-30A7200382B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Tests", "tests\DigitalData.MessagingService.Tests\DigitalData.MessagingService.Tests.csproj", "{211FB65F-2406-474E-A426-DA246B250AB8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4F20FEFD-9289-42C6-ABA6-8DB236D74559}"
@@ -25,42 +17,80 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
STATUS.md = STATUS.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{B52B4CEE-1C67-424B-8659-370FEA7EAF2A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{DD9D4A3A-AB55-456E-80D3-54A2D4025E64}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{71BEA4D0-7835-4A8C-B11E-1088E0801DCE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Application", "src\core\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj", "{7CBE8648-F259-CC91-87FF-5859280867A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Domain", "src\core\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj", "{8E44FA5B-43DD-E273-C682-FC382A854A6D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Publisher.Abstraction", "src\core\DigitalData.MessagingService.Publisher.Abstraction\DigitalData.MessagingService.Publisher.Abstraction.csproj", "{8A44EE33-02AA-8B3F-60F7-91BA483740A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Infrastructure", "src\infrastructure\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj", "{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.RabbitMQ", "src\infrastructure\DigitalData.MessagingService.RabbitMQ\DigitalData.MessagingService.RabbitMQ.csproj", "{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.API", "src\presentation\DigitalData.MessagingService.API\DigitalData.MessagingService.API.csproj", "{8BF22107-3CB9-C326-B94B-C40C99DA9B68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Client.DependencyInjection", "src\presentation\DigitalData.MessagingService.Client.DependencyInjection\DigitalData.MessagingService.Client.DependencyInjection.csproj", "{B67C6FA8-DA47-41EC-B15A-511C5B19C036}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{456817AB-67A3-49DD-9EC5-1253A727B958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Debug|Any CPU.Build.0 = Debug|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Release|Any CPU.ActiveCfg = Release|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Release|Any CPU.Build.0 = Release|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Release|Any CPU.Build.0 = Release|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Release|Any CPU.Build.0 = Release|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Release|Any CPU.Build.0 = Release|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Release|Any CPU.Build.0 = Release|Any CPU
{7CBE8648-F259-CC91-87FF-5859280867A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CBE8648-F259-CC91-87FF-5859280867A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CBE8648-F259-CC91-87FF-5859280867A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CBE8648-F259-CC91-87FF-5859280867A8}.Release|Any CPU.Build.0 = Release|Any CPU
{8E44FA5B-43DD-E273-C682-FC382A854A6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E44FA5B-43DD-E273-C682-FC382A854A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E44FA5B-43DD-E273-C682-FC382A854A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E44FA5B-43DD-E273-C682-FC382A854A6D}.Release|Any CPU.Build.0 = Release|Any CPU
{8A44EE33-02AA-8B3F-60F7-91BA483740A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A44EE33-02AA-8B3F-60F7-91BA483740A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A44EE33-02AA-8B3F-60F7-91BA483740A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A44EE33-02AA-8B3F-60F7-91BA483740A9}.Release|Any CPU.Build.0 = Release|Any CPU
{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2}.Release|Any CPU.Build.0 = Release|Any CPU
{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705}.Release|Any CPU.Build.0 = Release|Any CPU
{8BF22107-3CB9-C326-B94B-C40C99DA9B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BF22107-3CB9-C326-B94B-C40C99DA9B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BF22107-3CB9-C326-B94B-C40C99DA9B68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BF22107-3CB9-C326-B94B-C40C99DA9B68}.Release|Any CPU.Build.0 = Release|Any CPU
{B67C6FA8-DA47-41EC-B15A-511C5B19C036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B67C6FA8-DA47-41EC-B15A-511C5B19C036}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B67C6FA8-DA47-41EC-B15A-511C5B19C036}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B67C6FA8-DA47-41EC-B15A-511C5B19C036}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{456817AB-67A3-49DD-9EC5-1253A727B958} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{D2C32417-FE92-4063-97A9-3DA8D811E561} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{1874A827-C6A5-EB5E-0FE9-30A7200382B7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{211FB65F-2406-474E-A426-DA246B250AB8} = {4F20FEFD-9289-42C6-ABA6-8DB236D74559}
{B52B4CEE-1C67-424B-8659-370FEA7EAF2A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{DD9D4A3A-AB55-456E-80D3-54A2D4025E64} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{71BEA4D0-7835-4A8C-B11E-1088E0801DCE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{7CBE8648-F259-CC91-87FF-5859280867A8} = {DD9D4A3A-AB55-456E-80D3-54A2D4025E64}
{8E44FA5B-43DD-E273-C682-FC382A854A6D} = {DD9D4A3A-AB55-456E-80D3-54A2D4025E64}
{8A44EE33-02AA-8B3F-60F7-91BA483740A9} = {DD9D4A3A-AB55-456E-80D3-54A2D4025E64}
{56607AAB-3DEC-CB78-3062-56A8EEF5E9D2} = {71BEA4D0-7835-4A8C-B11E-1088E0801DCE}
{4CF993A6-FA3E-CBF7-C4CB-FFAEBFCFF705} = {71BEA4D0-7835-4A8C-B11E-1088E0801DCE}
{8BF22107-3CB9-C326-B94B-C40C99DA9B68} = {B52B4CEE-1C67-424B-8659-370FEA7EAF2A}
{B67C6FA8-DA47-41EC-B15A-511C5B19C036} = {B52B4CEE-1C67-424B-8659-370FEA7EAF2A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {90E29FDC-F6C6-414F-94BF-25DF61D18060}

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,28 +0,0 @@
namespace DigitalData.MessagingService.Application.Common.Events;
public class OutgoingEmailEvent
{
public required Guid Id { get; init; }
/// <summary>
/// Recipient email address
/// </summary>
public required string Recipient { get; init; }
/// <summary>
/// Email subject
/// </summary>
public required string Subject { get; init; }
/// <summary>
/// Email body (HTML or plain text)
/// </summary>
public required string Body { get; init; }
/// <summary>
/// Is HTML email (default: true)
/// </summary>
public bool IsHtml { get; init; } = true;
public DateTime QueuedAt { get; init; }
}

View File

@@ -1,9 +0,0 @@
namespace DigitalData.MessagingService.Domain.Common;
public abstract class BaseEntity
{
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
public string? ModifiedBy { get; set; }
}

View File

@@ -1,19 +0,0 @@
namespace DigitalData.MessagingService.Domain.Common;
/// <summary>
/// Domain-wide constants
/// </summary>
public static class DomainConstants
{
/// <summary>
/// Email processing constants
/// </summary>
public static class Email
{
/// <summary>
/// Maximum number of retry attempts for failed email sending
/// After this limit, email will be moved to Dead Letter Queue (DLQ)
/// </summary>
public const int MaxRetryCount = 3;
}
}

View File

@@ -1,8 +0,0 @@
namespace DigitalData.MessagingService.Domain.Common;
/// <summary>
/// Marker interface for aggregate roots in DDD
/// </summary>
public interface IAggregateRoot
{
}

View File

@@ -1,43 +0,0 @@
namespace DigitalData.MessagingService.Domain.Common;
/// <summary>
/// Base class for value objects that implement equality by value
/// </summary>
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
public static bool operator !=(ValueObject? left, ValueObject? right)
{
return !(left == right);
}
}

View File

@@ -1,9 +0,0 @@
namespace DigitalData.MessagingService.Domain.Enums;
public enum AttachmentStatus
{
Pending = 1,
Valid = 2,
Corrupt = 3,
Skipped = 4
}

View File

@@ -1,7 +0,0 @@
namespace DigitalData.MessagingService.Domain.Enums;
public enum AuthenticationType
{
UsernamePassword = 1,
OAuth2 = 2
}

View File

@@ -1,11 +0,0 @@
namespace DigitalData.MessagingService.Domain.Enums;
public enum EmailStatus
{
Pending = 1,
Processing = 2,
Processed = 3,
Failed = 4,
PartiallyProcessed = 5,
Rejected = 6
}

View File

@@ -1,16 +0,0 @@
namespace DigitalData.MessagingService.Domain.Enums;
public enum ErrorCode
{
None = 0,
NoAttachments = 10001,
SenderValidationFailed = 10002,
EmbeddedFileAttachmentCorrupt = 10003,
NormalFileAttachmentCorrupt = 10004,
PdfStructureInvalid = 10005,
ImapConnectionFailed = 10006,
WindreamImportFailed = 10007,
DiskSpaceInsufficient = 10008,
DuplicateMessageId = 10009,
AttachmentExtractionFailed = 10010
}

View File

@@ -1,8 +0,0 @@
namespace DigitalData.MessagingService.Domain.Enums;
public enum ProcessType
{
ProcessManager = 1, // Easy Approval workflow
AttachmentSniffer = 2, // General attachment extraction
ZugFeRDParser = 3 // Electronic invoice processing
}

View File

@@ -1,15 +0,0 @@
using DigitalData.MessagingService.Domain.Enums;
namespace DigitalData.MessagingService.Domain.Exceptions;
public class AttachmentProcessingException : DomainException
{
public AttachmentProcessingException(ErrorCode errorCode, string message) : base(message, errorCode)
{
}
public AttachmentProcessingException(ErrorCode errorCode, string message, Exception innerException)
: base(message, errorCode)
{
}
}

View File

@@ -1,17 +0,0 @@
namespace DigitalData.MessagingService.Domain.Exceptions;
/// <summary>
/// Exception thrown when OAuth2 authentication fails.
/// </summary>
public class AuthenticationFailedException : Exception
{
public AuthenticationFailedException(string message)
: base(message)
{
}
public AuthenticationFailedException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,22 +0,0 @@
namespace DigitalData.MessagingService.Domain.Exceptions;
/// <summary>
/// Exception thrown when DMS (windream) is not available or not configured properly.
/// </summary>
public class DmsNotAvailableException : Exception
{
public DmsNotAvailableException()
: base("DMS service is not available. windream COM components may not be registered.")
{
}
public DmsNotAvailableException(string message)
: base(message)
{
}
public DmsNotAvailableException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,21 +0,0 @@
using DigitalData.MessagingService.Domain.Enums;
namespace DigitalData.MessagingService.Domain.Exceptions;
public class DomainException : Exception
{
public ErrorCode? ErrorCode { get; }
public DomainException(string message) : base(message)
{
}
public DomainException(string message, ErrorCode errorCode) : base(message)
{
ErrorCode = errorCode;
}
public DomainException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,17 +0,0 @@
namespace DigitalData.MessagingService.Domain.Exceptions;
/// <summary>
/// Exception thrown when a PDF file is invalid or corrupted.
/// </summary>
public class InvalidPdfException : Exception
{
public InvalidPdfException(string message)
: base(message)
{
}
public InvalidPdfException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,22 +0,0 @@
namespace DigitalData.MessagingService.Domain.Exceptions;
/// <summary>
/// Exception thrown when a requested entity is not found.
/// </summary>
public class NotFoundException : Exception
{
public NotFoundException(string entityName, object key)
: base($"{entityName} with key '{key}' was not found.")
{
}
public NotFoundException(string message)
: base(message)
{
}
public NotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,18 +0,0 @@
using DigitalData.MessagingService.Domain.Enums;
namespace DigitalData.MessagingService.Domain.Exceptions;
public class ValidationException : DomainException
{
public ValidationException(string message) : base(message)
{
}
public ValidationException(string message, ErrorCode errorCode) : base(message, errorCode)
{
}
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,28 +0,0 @@
using DigitalData.MessagingService.Domain.ValueObjects;
namespace DigitalData.MessagingService.Domain.Services;
public interface IMessageIdGenerator
{
MessageId Generate(string originalMessageId, string sender, DateTime date, string subject);
List<string> GenerateFallbackHashes(MessageId messageId);
}
public class MessageIdGenerator : IMessageIdGenerator
{
public MessageId Generate(string originalMessageId, string sender, DateTime date, string subject)
{
return MessageId.Create(originalMessageId, sender, date, subject);
}
public List<string> GenerateFallbackHashes(MessageId messageId)
{
// Legacy behavior: 10 variations for duplicate detection
var hashes = new List<string> { messageId.Hash };
// Add variations (this is simplified - legacy had 10 variations)
// TODO: Implement exact legacy fallback algorithm if needed
return hashes;
}
}

View File

@@ -1,45 +0,0 @@
using System.ComponentModel.DataAnnotations;
using DigitalData.MessagingService.Domain.Common;
using DigitalData.MessagingService.Domain.Exceptions;
namespace DigitalData.MessagingService.Domain.ValueObjects;
/// <summary>
/// Value object representing an email address with validation
/// </summary>
public class EmailAddress : ValueObject
{
public string Value { get; private set; }
public string Domain { get; private set; }
public string LocalPart { get; private set; }
private EmailAddress(string value)
{
Value = value;
var parts = value.Split('@');
LocalPart = parts[0];
Domain = parts[1];
}
public static EmailAddress Create(string email)
{
if (!IsValid(email))
throw new DomainException($"Invalid email address: {email}");
return new EmailAddress(email.ToLowerInvariant());
}
private static bool IsValid(string email)
{
return !string.IsNullOrWhiteSpace(email) &&
email.Contains('@') &&
new EmailAddressAttribute().IsValid(email);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public override string ToString() => Value;
}

View File

@@ -1,41 +0,0 @@
using System.Security.Cryptography;
using System.Text;
using DigitalData.MessagingService.Domain.Common;
namespace DigitalData.MessagingService.Domain.ValueObjects;
/// <summary>
/// Value object representing a unique message identifier with hash generation
/// Uses the same algorithm as legacy system for compatibility
/// </summary>
public class MessageId : ValueObject
{
public string Value { get; private set; }
public string Hash { get; private set; }
private MessageId(string value)
{
Value = value;
Hash = GenerateHash(value);
}
public static MessageId Create(string originalMessageId, string sender, DateTime date, string subject)
{
var combined = $"{originalMessageId}|{sender}|{date:yyyyMMddHHmmss}|{subject}";
return new MessageId(combined);
}
private static string GenerateHash(string input)
{
// Same algorithm as legacy: SHA256 hash
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}

View File

@@ -1,54 +0,0 @@
namespace DigitalData.MessagingService.Infrastructure.Messaging;
/// <summary>
/// Configuration for RabbitMQ connection
/// </summary>
public class RabbitMqConfiguration
{
/// <summary>
/// Configuration section name in appsettings.json
/// </summary>
public const string SectionName = "RabbitMQ";
/// <summary>
/// RabbitMQ server hostname
/// </summary>
public string HostName { get; set; } = "localhost";
/// <summary>
/// RabbitMQ AMQP port (default: 5672)
/// </summary>
public int Port { get; set; } = 5672;
/// <summary>
/// RabbitMQ username
/// </summary>
public string UserName { get; set; } = "guest";
/// <summary>
/// RabbitMQ password
/// </summary>
public string Password { get; set; } = "guest";
/// <summary>
/// Virtual host (default: /)
/// </summary>
public string VirtualHost { get; set; } = "/";
/// <summary>
/// Enable automatic recovery on connection failure
/// </summary>
public bool AutomaticRecoveryEnabled { get; set; } = true;
/// <summary>
/// Network recovery interval in seconds
/// </summary>
public int NetworkRecoveryIntervalSeconds { get; set; } = 10;
public string QueueName { get; set; } = null!;
public string ExchangeName { get; set; } = null!;
public string RoutingKey { get; set; } = null!;
public string DlqQueueName { get; set; } = null!;
public string DlqExchangeName { get; set; } = null!;
public string DlqRoutingKey { get; set; } = null!;
}

View File

@@ -1,217 +0,0 @@
using System.Text;
using System.Text.Json;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Messaging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using DigitalData.MessagingService.Application.Common.Events;
namespace DigitalData.MessagingService.Infrastructure.Queue;
/// <summary>
/// RabbitMQ-based email queue implementation for outgoing emails.
/// Provides message persistence, scalability, and reliability.
/// Uses Lazy<T> initialization pattern to avoid blocking constructor.
/// </summary>
public sealed class OutgoingEmailQueue(IOptions<RabbitMqConfiguration> config, ILogger<OutgoingEmailQueue> Logger, IEmailService EmailService) : IOutgoingEmailQueue, IAsyncDisposable
{
private readonly RabbitMqConfiguration _config = config.Value;
private IConnection? _connection = null;
private IChannel? _publishChannel = null; // Dedicated channel for publishing
private IChannel? _consumeChannel = null; // Dedicated channel for consuming
private readonly CancellationTokenSource _consumerCts = new(); // Independent lifetime from InitAsync token
/// <summary>
/// Initialize RabbitMQ connection, channel, exchanges, and queues asynchronously.
/// Called lazily on first use via EnsureInitializedAsync.
/// </summary>
public async Task InitAsync(CancellationToken stoppingToken = default)
{
Logger.LogInformation("Initializing RabbitMQ connection and queues...");
var factory = new ConnectionFactory
{
HostName = _config.HostName,
Port = _config.Port,
UserName = _config.UserName,
Password = _config.Password,
VirtualHost = _config.VirtualHost,
AutomaticRecoveryEnabled = _config.AutomaticRecoveryEnabled,
NetworkRecoveryInterval = TimeSpan.FromSeconds(_config.NetworkRecoveryIntervalSeconds)
};
_connection = await factory.CreateConnectionAsync(stoppingToken);
// Separate channels: IChannel is not thread-safe; one per role is best practice
_publishChannel = await _connection.CreateChannelAsync(cancellationToken: stoppingToken);
_consumeChannel = await _connection.CreateChannelAsync(cancellationToken: stoppingToken);
// Topology declaration can use either channel; use publish channel here
// Declare Dead Letter Queue (DLQ) exchange
await _publishChannel.ExchangeDeclareAsync(exchange: _config.DlqExchangeName, type: ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: stoppingToken);
// Declare Dead Letter Queue (DLQ)
await _publishChannel.QueueDeclareAsync(queue: _config.DlqQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: stoppingToken);
// Bind DLQ to DLQ exchange
await _publishChannel.QueueBindAsync(queue: _config.DlqQueueName, exchange: _config.DlqExchangeName, routingKey: _config.DlqRoutingKey, cancellationToken: stoppingToken);
// Declare main exchange (Direct type for routing)
await _publishChannel.ExchangeDeclareAsync(exchange: _config.ExchangeName, type: ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: stoppingToken);
// Declare main queue (durable for persistence) with DLQ arguments
var queueArgs = new Dictionary<string, object?>
{
{ "x-dead-letter-exchange", _config.DlqExchangeName },
{ "x-dead-letter-routing-key", _config.DlqRoutingKey }
};
await _publishChannel.QueueDeclareAsync(queue: _config.QueueName, durable: true, exclusive: false, autoDelete: false, arguments: queueArgs, cancellationToken: stoppingToken);
// Bind main queue to exchange with routing key
await _publishChannel.QueueBindAsync(queue: _config.QueueName, exchange: _config.ExchangeName, routingKey: _config.RoutingKey, cancellationToken: stoppingToken);
// Consumer uses its own CancellationToken independent of the startup token,
// so it keeps running after InitAsync completes or its token is cancelled.
// Link stoppingToken so the consumer stops when the host stops.
stoppingToken.Register(() => _consumerCts.Cancel());
await StartConsumerAsync(_consumeChannel, _consumerCts.Token);
Logger.LogInformation("RabbitMQ initialized successfully: Queue={QueueName}, DLQ={DlqQueueName}", _config.QueueName, _config.DlqQueueName);
}
public async Task EnqueueAsync(OutgoingEmailEvent outgoingEmailEvent, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(outgoingEmailEvent);
var body = Encoding.UTF8.GetBytes(json);
var properties = new BasicProperties
{
Persistent = true, // Message persistence
ContentType = "application/json",
Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeSeconds())
};
await _publishChannel!.BasicPublishAsync(
exchange: _config.ExchangeName,
routingKey: _config.RoutingKey,
mandatory: false,
basicProperties: properties,
body: body,
cancellationToken: cancellationToken);
}
public async Task<int> GetQueueDepthAsync(CancellationToken cancellationToken = default)
{
var queueInfo = await _publishChannel!.QueueDeclarePassiveAsync(_config.QueueName, cancellationToken);
return (int)queueInfo.MessageCount;
}
/// <summary>
/// Start event-driven consumer that processes messages as they arrive
/// </summary>
private async Task StartConsumerAsync(IChannel consumeChannel, CancellationToken cancellationToken)
{
var consumer = new AsyncEventingBasicConsumer(consumeChannel);
consumer.ReceivedAsync += async (sender, args) =>
{
OutgoingEmailEvent? oMailEvent = null;
try
{
var json = Encoding.UTF8.GetString(args.Body.ToArray());
oMailEvent = JsonSerializer.Deserialize<OutgoingEmailEvent>(json);
if (oMailEvent is not null)
{
Logger.LogDebug("Received email message: To={To}, Subject={Subject}", oMailEvent.Recipient, oMailEvent.Subject);
Logger.LogInformation("Processing outgoing email: To={To}, Subject={Subject}",
oMailEvent.Recipient, oMailEvent.Subject);
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
await EmailService.SendEmailAsync(
oMailEvent.Recipient,
oMailEvent.Subject,
oMailEvent.Body,
isHtml: oMailEvent.IsHtml);
Logger.LogInformation("Email sent successfully: To={To}, Subject={Subject}",
oMailEvent.Recipient, oMailEvent.Subject);
// Acknowledge message after successful processing
await consumeChannel.BasicAckAsync(args.DeliveryTag, false, args.CancellationToken);
Logger.LogDebug("Message acknowledged: DeliveryTag={DeliveryTag}", args.DeliveryTag);
}
else
{
Logger.LogWarning("Failed to deserialize email message: DeliveryTag={DeliveryTag}", args.DeliveryTag);
await consumeChannel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // Don't requeue invalid messages
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipient, oMailEvent?.Subject, args.DeliveryTag);
// TODO: Error Reporting Strategy
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
// - Create EmailErrorReport entity { OutgoingEmailEventId, Exception, StackTrace, Timestamp, RetryAttempt }
// - Publish to error queue: await _errorQueue.EnqueueAsync(errorReport)
// - Separate worker processes error queue → Log to DB/File/External monitoring
//
// Option 2: Database Table (TBEMLP_ERROR_LOG)
// - Columns: ERROR_ID, OUTBOX_ID, ERROR_MESSAGE, STACK_TRACE, ERROR_DATE
// - Insert via IErrorLogRepository.CreateAsync(errorLog)
//
// Option 3: External Monitoring Service
// - Sentry: SentrySdk.CaptureException(ex)
// - Application Insights: _telemetryClient.TrackException(ex)
// - Elasticsearch: _elasticClient.IndexDocument(errorLog)
//
// Recommended: Option 1 (RabbitMQ Error Queue) + Option 2 (DB persistence)
// - Fast async error logging (non-blocking)
// - Persistent storage for audit
// - Real-time alerting via monitoring worker
// NO RETRY - All failures move directly to DLQ
await consumeChannel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // requeue=false → DLQ
}
};
// Start consuming messages (event-driven, non-blocking)
await consumeChannel.BasicConsumeAsync(
queue: _config.QueueName,
autoAck: false,
consumer: consumer,
cancellationToken: cancellationToken);
Logger.LogInformation("RabbitMQ consumer started for queue: {QueueName}", _config.QueueName);
}
public async ValueTask DisposeAsync()
{
await _consumerCts.CancelAsync();
_consumerCts.Dispose();
if (_consumeChannel is not null)
{
await _consumeChannel.CloseAsync();
await _consumeChannel.DisposeAsync();
}
if (_publishChannel is not null)
{
await _publishChannel.CloseAsync();
await _publishChannel.DisposeAsync();
}
if (_connection is not null)
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
}
}
}

View File

@@ -1,32 +0,0 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Queue;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DigitalData.MessagingService.Infrastructure.Services.Background;
/// <summary>
/// A hosted background service responsible for initializing the outgoing email queue consumer.
/// Leverages a push-based, event-driven RabbitMQ consumer to eliminate polling overhead.
/// Email account configuration is resolved exclusively from application settings; no database access is performed.
/// </summary>
public class AsyncInitWorker(IOutgoingEmailQueue EmailQueue, ILogger<AsyncInitWorker> Logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("Outgoing email queue worker is starting. Initializing event-driven RabbitMQ consumer.");
try
{
// Initialize the RabbitMQ push-based consumer. This call is non-blocking;
// message processing is handled asynchronously via registered event callbacks.
if (EmailQueue is OutgoingEmailQueue outgoingEmailQueue)
await outgoingEmailQueue.InitAsync(stoppingToken);
}
catch (Exception ex)
{
Logger.LogError(ex, "A critical error occurred while initializing the outgoing email queue consumer. The worker cannot proceed.");
throw;
}
}
}

View File

@@ -1,6 +1,6 @@
using AutoMapper;
using DigitalData.MessagingService.Application.Common.Events;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using DigitalData.MessagingService.Publisher.Abstraction;
namespace DigitalData.MessagingService.Application.Common.Mappings;

View File

@@ -15,7 +15,6 @@
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageReference Include="MimeKit" Version="4.17.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
</ItemGroup>

View File

@@ -1,6 +1,5 @@
using AutoMapper;
using DigitalData.MessagingService.Application.Common.Events;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Publisher.Abstraction;
using MediatR;
namespace DigitalData.MessagingService.Application.EmailSending.Commands;
@@ -35,14 +34,14 @@ public record SendEmailCommand : IRequest<OutgoingEmailEvent>
/// Handler for SendEmailCommand
/// Creates EmailOutbox entity via AutoMapper and enqueues to RabbitMQ
/// </summary>
public class SendEmailCommandHandler(IOutgoingEmailQueue EmailQueue, IMapper Mapper) : IRequestHandler<SendEmailCommand, OutgoingEmailEvent>
public class SendEmailCommandHandler(IOutgoingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler<SendEmailCommand, OutgoingEmailEvent>
{
public async Task<OutgoingEmailEvent> Handle(SendEmailCommand request, CancellationToken cancellationToken)
{
var outgoingEmailEvent = Mapper.Map<OutgoingEmailEvent>(request);
// Enqueue to RabbitMQ
await EmailQueue.EnqueueAsync(outgoingEmailEvent, cancellationToken);
await Publisher.EnqueueAsync(outgoingEmailEvent, cancellationToken);
return outgoingEmailEvent;
}
}

View File

@@ -1,17 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net462;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Events\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
namespace DigitalData.MessagingService.Domain.Exceptions
{
/// <summary>
/// Exception thrown when OAuth2 authentication fails.
/// </summary>
public class AuthenticationFailedException : Exception
{
public AuthenticationFailedException(string message) : base(message)
{
}
public AuthenticationFailedException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,21 @@
namespace DigitalData.MessagingService.Domain.Exceptions
{
/// <summary>
/// Exception thrown when a requested entity is not found.
/// </summary>
public class NotFoundException : Exception
{
public NotFoundException(string entityName, object key) : base($"{entityName} with key '{key}' was not found.")
{
}
public NotFoundException(string message) : base(message)
{
}
public NotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>

View File

@@ -1,12 +1,12 @@
using DigitalData.MessagingService.Application.Common.Events;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using System.Threading;
using System.Threading.Tasks;
namespace DigitalData.MessagingService.Application.Common.Interfaces;
namespace DigitalData.MessagingService.Publisher.Abstraction;
/// <summary>
/// Email queue interface for outgoing emails.
/// </summary>
public interface IOutgoingEmailQueue
public interface IOutgoingEmailPublisher
{
Task EnqueueAsync(OutgoingEmailEvent outgoingEmailEvent, CancellationToken cancellationToken = default);

View File

@@ -0,0 +1,30 @@
using System;
namespace DigitalData.MessagingService.Publisher.Abstraction;
public class OutgoingEmailEvent
{
public Guid Id { get; set; }
/// <summary>
/// Recipient email address
/// </summary>
public string Recipient { get; set; } = null!;
/// <summary>
/// Email subject
/// </summary>
public string Subject { get; set; } = null!;
/// <summary>
/// Email body (HTML or plain text)
/// </summary>
public string Body { get; set; } = null!;
/// <summary>
/// Is HTML email (default: true)
/// </summary>
public bool IsHtml { get; set; } = true;
public DateTime QueuedAt { get; set; }
}

View File

@@ -1,8 +1,9 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Messaging;
using DigitalData.MessagingService.Infrastructure.Queue;
using DigitalData.MessagingService.Infrastructure.Services;
using DigitalData.MessagingService.Infrastructure.Services.Background;
using DigitalData.MessagingService.Publisher.Abstraction;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -32,11 +33,11 @@ public static class DependencyInjection
services.AddSingleton<IEncryptionService, DataProtectionEncryptionService>();
// --- Email Queue (RabbitMQ) ---
services.AddSingleton<IOutgoingEmailQueue, OutgoingEmailQueue>();
services.AddSingleton<OutgoingEmailConsumer>();
services.AddSingleton<IOutgoingEmailPublisher, OutgoingEmailPublisher>();
// --- RabbitMQ Configuration ---
services.Configure<RabbitMqConfiguration>(
configuration.GetSection(RabbitMqConfiguration.SectionName));
services.AddRabbitMqConnectionFactory(configuration);
// --- Data Protection (for encryption) ---
services.AddDataProtection()

View File

@@ -9,12 +9,12 @@
<ItemGroup>
<ProjectReference Include="..\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.RabbitMQ\DigitalData.MessagingService.RabbitMQ.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="DevExpress.Document.Processor" Version="26.1.3" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
@@ -34,4 +34,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="Messaging\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,130 @@
using System.Text;
using System.Text.Json;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Publisher.Abstraction;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace DigitalData.MessagingService.Infrastructure.Queue;
/// <summary>
/// RabbitMQ-based email queue implementation for outgoing emails.
/// Provides message persistence, scalability, and reliability.
/// Uses Lazy<T> initialization pattern to avoid blocking constructor.
/// </summary>
public sealed class OutgoingEmailConsumer : IAsyncDisposable
{
private readonly RabbitMqConfiguration _config;
private readonly Lazy<Task<IChannel>> _lazyChannel;
private readonly Lazy<Task> _lazyInit;
private readonly ILogger<OutgoingEmailConsumer>? _logger;
public OutgoingEmailConsumer(IOptions<RabbitMqConfiguration> config, IEmailService EmailService, RabbitMqConnectionFactory CnnFactory, ILogger<OutgoingEmailConsumer>? logger = null)
{
_logger = logger;
_config = config.Value;
_lazyChannel = new(CnnFactory.CreateChannelAsync);
_lazyInit = new(async () => {
var channel = await _lazyChannel.Value;
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (sender, args) =>
{
OutgoingEmailEvent? oMailEvent = null;
try
{
var json = Encoding.UTF8.GetString(args.Body.ToArray());
oMailEvent = JsonSerializer.Deserialize<OutgoingEmailEvent>(json);
if (oMailEvent is not null)
{
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
await EmailService.SendEmailAsync(
oMailEvent.Recipient,
oMailEvent.Subject,
oMailEvent.Body,
isHtml: oMailEvent.IsHtml);
// Acknowledge message after successful processing
await channel.BasicAckAsync(args.DeliveryTag, false, args.CancellationToken);
}
else
{
logger?.LogWarning("Failed to deserialize email message: DeliveryTag={DeliveryTag}", args.DeliveryTag);
await channel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // Don't requeue invalid messages
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipient, oMailEvent?.Subject, args.DeliveryTag);
// TODO: Error Reporting Strategy
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
// - Create EmailErrorReport entity { OutgoingEmailEventId, Exception, StackTrace, Timestamp, RetryAttempt }
// - Publish to error queue: await _errorQueue.EnqueueAsync(errorReport)
// - Separate worker processes error queue → Log to DB/File/External monitoring
//
// Option 2: Database Table (TBEMLP_ERROR_LOG)
// - Columns: ERROR_ID, OUTBOX_ID, ERROR_MESSAGE, STACK_TRACE, ERROR_DATE
// - Insert via IErrorLogRepository.CreateAsync(errorLog)
//
// Option 3: External Monitoring Service
// - Sentry: SentrySdk.CaptureException(ex)
// - Application Insights: _telemetryClient.TrackException(ex)
// - Elasticsearch: _elasticClient.IndexDocument(errorLog)
//
// Recommended: Option 1 (RabbitMQ Error Queue) + Option 2 (DB persistence)
// - Fast async error logging (non-blocking)
// - Persistent storage for audit
// - Real-time alerting via monitoring worker
// NO RETRY - All failures move directly to DLQ
await channel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // requeue=false → DLQ
}
};
// Start consuming messages (event-driven, non-blocking)
await channel.BasicConsumeAsync(
queue: _config.QueueName,
autoAck: false,
consumer: consumer,
cancellationToken: CnnFactory.CancellationToken);
logger?.LogInformation("RabbitMQ consumer started for queue: {QueueName}", _config.QueueName);
});
}
/// <summary>
/// Initialize RabbitMQ connection, channel, exchanges, and queues asynchronously.
/// Start event-driven consumer that processes messages as they arrive
/// Called lazily on first use via EnsureInitializedAsync.
/// </summary>
public async Task InitAsync()
{
if (_lazyInit.IsValueCreated)
_logger?.LogWarning("OutgoingEmailConsumer already initialized. InitAsync() called multiple times.");
await _lazyInit.Value;
}
public async ValueTask DisposeAsync()
{
if (!_lazyChannel.IsValueCreated)
return;
var channel = await _lazyChannel.Value;
if (channel is not null)
{
await channel.CloseAsync();
await channel.DisposeAsync();
}
}
}

View File

@@ -0,0 +1,107 @@
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using DigitalData.MessagingService.RabbitMQ;
using DigitalData.MessagingService.Publisher.Abstraction;
namespace DigitalData.MessagingService.Infrastructure.Queue;
/// <summary>
/// RabbitMQ-based email queue implementation for outgoing emails.
/// Provides message persistence, scalability, and reliability.
/// Uses Lazy<T> initialization pattern to avoid blocking constructor.
/// </summary>
public sealed class OutgoingEmailPublisher : IOutgoingEmailPublisher, IAsyncDisposable
{
private readonly RabbitMqConfiguration _config;
private readonly ILogger<OutgoingEmailPublisher> _logger;
private readonly RabbitMqConnectionFactory _cnnFactory;
private readonly Lazy<Task<IChannel>> _lazyChannel;
public OutgoingEmailPublisher(IOptions<RabbitMqConfiguration> config, ILogger<OutgoingEmailPublisher> logger, RabbitMqConnectionFactory cnnFactory)
{
_config = config.Value;
_logger = logger;
_cnnFactory = cnnFactory;
_lazyChannel = new(InitChannelAsync);
}
/// <summary>
/// Initialize RabbitMQ connection, channel, exchanges, and queues asynchronously.
/// Called lazily on first use via EnsureInitializedAsync.
/// </summary>
private async Task<IChannel> InitChannelAsync()
{
var channel = await _cnnFactory.CreateChannelAsync();
// Topology declaration can use either channel; use publish channel here
// Declare Dead Letter Queue (DLQ) exchange
await channel.ExchangeDeclareAsync(exchange: _config.DlqExchangeName, type: ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: _cnnFactory.CancellationToken);
// Declare Dead Letter Queue (DLQ)
await channel.QueueDeclareAsync(queue: _config.DlqQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: _cnnFactory.CancellationToken);
// Bind DLQ to DLQ exchange
await channel.QueueBindAsync(queue: _config.DlqQueueName, exchange: _config.DlqExchangeName, routingKey: _config.DlqRoutingKey, cancellationToken: _cnnFactory.CancellationToken);
// Declare main exchange (Direct type for routing)
await channel.ExchangeDeclareAsync(exchange: _config.ExchangeName, type: ExchangeType.Direct, durable: true, autoDelete: false, cancellationToken: _cnnFactory.CancellationToken);
// Declare main queue (durable for persistence) with DLQ arguments
var queueArgs = new Dictionary<string, object?>
{
{ "x-dead-letter-exchange", _config.DlqExchangeName },
{ "x-dead-letter-routing-key", _config.DlqRoutingKey }
};
await channel.QueueDeclareAsync(queue: _config.QueueName, durable: true, exclusive: false, autoDelete: false, arguments: queueArgs, cancellationToken: _cnnFactory.CancellationToken);
// Bind main queue to exchange with routing key
await channel.QueueBindAsync(queue: _config.QueueName, exchange: _config.ExchangeName, routingKey: _config.RoutingKey, cancellationToken: _cnnFactory.CancellationToken);
_logger.LogInformation("RabbitMQ initialized successfully: Queue={QueueName}, DLQ={DlqQueueName}", _config.QueueName, _config.DlqQueueName);
return channel;
}
public async Task EnqueueAsync(OutgoingEmailEvent outgoingEmailEvent, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(outgoingEmailEvent);
var body = Encoding.UTF8.GetBytes(json);
var properties = new BasicProperties
{
Persistent = true, // Message persistence
ContentType = "application/json",
Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeSeconds())
};
var channel = await _lazyChannel.Value;
await channel.BasicPublishAsync(
exchange: _config.ExchangeName,
routingKey: _config.RoutingKey,
mandatory: false,
basicProperties: properties,
body: body,
cancellationToken: cancellationToken);
}
public async Task<int> GetQueueDepthAsync(CancellationToken cancellationToken = default)
{
var channel = await _lazyChannel.Value;
var queueInfo = await channel.QueueDeclarePassiveAsync(_config.QueueName, cancellationToken);
return (int)queueInfo.MessageCount;
}
public async ValueTask DisposeAsync()
{
if (await _lazyChannel.Value is IChannel channel)
{
await channel.CloseAsync();
await channel.DisposeAsync();
}
}
}

View File

@@ -0,0 +1,20 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Queue;
using Microsoft.Extensions.Hosting;
namespace DigitalData.MessagingService.Infrastructure.Services.Background;
/// <summary>
/// A hosted background service responsible for initializing the outgoing email queue consumer.
/// Leverages a push-based, event-driven RabbitMQ consumer to eliminate polling overhead.
/// Email account configuration is resolved exclusively from application settings; no database access is performed.
/// </summary>
public class AsyncInitWorker(OutgoingEmailConsumer EmailConsumer) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await EmailConsumer.InitAsync();
await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DigitalData.MessagingService.RabbitMQ
{
/// <summary>
/// Dependency injection configuration for Infrastructure layer
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Adds Infrastructure layer services to the DI container
/// </summary>
public static IServiceCollection AddRabbitMqConnectionFactory(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddSingleton<RabbitMqConnectionFactory>();
// --- RabbitMQ Configuration ---
services.Configure<RabbitMqConfiguration>(
configuration.GetSection(RabbitMqConfiguration.SectionName));
return services;
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,55 @@
namespace DigitalData.MessagingService.RabbitMQ
{
/// <summary>
/// Configuration for RabbitMQ connection
/// </summary>
public class RabbitMqConfiguration
{
/// <summary>
/// Configuration section name in appsettings.json
/// </summary>
public const string SectionName = "RabbitMQ";
/// <summary>
/// RabbitMQ server hostname
/// </summary>
public string HostName { get; set; } = "localhost";
/// <summary>
/// RabbitMQ AMQP port (default: 5672)
/// </summary>
public int Port { get; set; } = 5672;
/// <summary>
/// RabbitMQ username
/// </summary>
public string UserName { get; set; } = "guest";
/// <summary>
/// RabbitMQ password
/// </summary>
public string Password { get; set; } = "guest";
/// <summary>
/// Virtual host (default: /)
/// </summary>
public string VirtualHost { get; set; } = "/";
/// <summary>
/// Enable automatic recovery on connection failure
/// </summary>
public bool AutomaticRecoveryEnabled { get; set; } = true;
/// <summary>
/// Network recovery interval in seconds
/// </summary>
public int NetworkRecoveryIntervalSeconds { get; set; } = 10;
public string QueueName { get; set; }
public string ExchangeName { get; set; }
public string RoutingKey { get; set; }
public string DlqQueueName { get; set; }
public string DlqExchangeName { get; set; }
public string DlqRoutingKey { get; set; }
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DigitalData.MessagingService.RabbitMQ
{
public sealed class RabbitMqConnectionFactory : IAsyncDisposable
{
private readonly CancellationTokenSource _consumerCts = new CancellationTokenSource();
private readonly RabbitMqConfiguration _config;
private readonly ILogger<RabbitMqConnectionFactory>
#if nullable
?
#endif
_logger;
private readonly Lazy<Task<IConnection>> _lazyConnectionProvider;
public CancellationToken CancellationToken => _consumerCts.Token;
public Task<IConnection> GetDefaultConnectionAsync()
{
return _lazyConnectionProvider.Value;
}
public async Task<IChannel> CreateChannelAsync()
{
var cnn = await GetDefaultConnectionAsync();
return await cnn.CreateChannelAsync(cancellationToken: CancellationToken);
}
public async Task<AsyncEventingBasicConsumer> CreateConsumerAsync()
{
var channel = await CreateChannelAsync();
return new AsyncEventingBasicConsumer(channel);
}
public RabbitMqConnectionFactory(IOptions<RabbitMqConfiguration> config)
{
_config = config.Value;
_lazyConnectionProvider = new Lazy<Task<IConnection>>(async () =>
{
var factory = new ConnectionFactory
{
HostName = _config.HostName,
Port = _config.Port,
UserName = _config.UserName,
Password = _config.Password,
VirtualHost = _config.VirtualHost,
AutomaticRecoveryEnabled = _config.AutomaticRecoveryEnabled,
NetworkRecoveryInterval = TimeSpan.FromSeconds(_config.NetworkRecoveryIntervalSeconds),
};
return await factory.CreateConnectionAsync(CancellationToken);
});
}
public async ValueTask DisposeAsync()
{
#if NET
await _consumerCts.CancelAsync();
#else
_consumerCts.Cancel();
#endif
var connection = await _lazyConnectionProvider.Value;
if (connection != null)
{
await connection.CloseAsync();
await connection.DisposeAsync();
}
}
}
}

View File

@@ -48,13 +48,7 @@ public class ExceptionHandlingMiddleware
AuthenticationFailedException authEx =>
(HttpStatusCode.Unauthorized, authEx.Message),
DmsNotAvailableException dmsEx =>
(HttpStatusCode.ServiceUnavailable, dmsEx.Message),
InvalidPdfException pdfEx =>
(HttpStatusCode.BadRequest, pdfEx.Message),
FluentValidation.ValidationException validationEx =>
(HttpStatusCode.BadRequest, FormatValidationErrors(validationEx)),

View File

@@ -0,0 +1,27 @@
{
"AllowedHosts": "*",
"RabbitMQ": {
"HostName": "172.24.12.56",
"Port": 5672,
"UserName": "admin",
"Password": "fl!'D}4;pYBb\\VD&{6]]G*\\0Bq8fVIn0j?Sgm\\2A,6GS47g5Dj",
"VirtualHost": "/",
"AutomaticRecoveryEnabled": true,
"NetworkRecoveryIntervalSeconds": 10,
"QueueName": "emailprofiler.email.outbox",
"ExchangeName": "emailprofiler.emails",
"RoutingKey": "email.outbox",
"DlqQueueName": "emailprofiler.email.outbox.dlq",
"DlqExchangeName": "emailprofiler.emails.dlq",
"DlqRoutingKey": "email.outbox.dlq"
},
"EmailAccount": {
"Username": "test-flow@digitaldata.works",
"Password": "ddemail108",
"PasswordEncrypted": false,
"SmtpServer": "kundencenter.triplew.de",
"SmtpPort": 465,
"SmtpUseSsl": true,
"UseOAuth2": false
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(MSBuildProjectName).xml</DocumentationFile>
<PackageId>DigitalData.MessagingService.Client</PackageId>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>DigitalData.MessagingService.Client</Product>
<Copyright>Copyright 2026</Copyright>
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
<PackageTags>digital data messaging service api client</PackageTags>
<Version>1.0.0-beta</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Description></Description>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\..\assets\icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>