Compare commits

...

11 Commits

Author SHA1 Message Date
6183ea613f Add ConnectRabbitMq method to EmailSender class
Introduce a new static method `ConnectRabbitMq` in the `EmailSender` class to configure and establish RabbitMQ connections using a URL, username, and password. The method extracts connection details (host, port, virtual host) from the URL and supports optional behavior for reconnection scenarios via the `onReconnect` parameter. Added `using System;` to support the `Uri` class.
2026-07-28 17:26:23 +02:00
e68fa989e1 Add defaults and docs to RabbitMqConfiguration properties
Updated the `RabbitMqConfiguration` class to include:
- XML documentation for all properties, improving clarity.
- Default values for `QueueName`, `ExchangeName`, `RoutingKey`,
  `DlqQueueName`, `DlqExchangeName`, and `DlqRoutingKey` to define
  RabbitMQ naming conventions.
Replaced undocumented properties with documented versions to enhance
code readability and maintainability.
2026-07-28 15:54:38 +02:00
de44b1967f Remove unused project references from test project 2026-07-28 14:33:51 +02:00
6eebfed97e Add EmailSender static client with lazy-initialized DI container
- Add EmailSender class with ConnectRabbitMq and Send methods
- Add OnReconnect enum for reconnection behavior control
- Add required NuGet packages (DependencyInjection, Hosting.Abstractions)
- Add project references to Publisher.Abstraction and Publisher
2026-07-28 14:33:47 +02:00
e8359abafd Simplify Publisher DI by removing Configuration wrapper class 2026-07-28 14:33:41 +02:00
a38e8f9680 Add Action<RabbitMqConfiguration> overload to RabbitMQ DI registration 2026-07-28 14:33:36 +02:00
7f55d97352 Refactor email publisher integration
Replaced direct registration of `OutgoingEmailPublisher` with `AddMessagingServicePublisher()` to centralize publisher setup. Removed `OutgoingEmailPublisher.cs` and its dependencies, indicating a shift to a new implementation. Updated `DependencyInjection.cs` to use `DigitalData.MessagingService.Publisher` instead of the abstraction layer. Added a project reference to `DigitalData.MessagingService.Publisher` in the infrastructure project file.
2026-07-28 11:14:42 +02:00
472c9506f9 Add RabbitMQ-based email publisher and DI support
Introduced `OutgoingEmailPublisher` for RabbitMQ-based email
queueing with message persistence, scalability, and reliability.
Added dependency injection support via `AddMessagingServicePublisher`
extension method. Enhanced RabbitMQ topology setup with exchanges,
queues, and Dead Letter Queues (DLQ).

Updated `DigitalData.MessagingService.Publisher.csproj` and
`DigitalData.MessagingService.RabbitMQ.csproj` to support
`net462`, `net480`, and `net8.0`. Added project references
and conditional package references for compatibility.

Integrated logging with `Microsoft.Extensions.Logging` and
used `System.Text.Json` for serialization. Implemented lazy
initialization for RabbitMQ channels to improve performance.
2026-07-28 11:06:16 +02:00
47f4553986 Refactor project structure and update dependencies
Reorganized project structure by introducing `core` and
`infrastructure` directories:
- Moved `Application` and `Domain` projects to `core`.
- Moved `Infrastructure` project to `infrastructure`.

Updated project references in `API`, `Infrastructure`, and
`Application` projects to reflect the new directory structure.

Added a new dependency on `Publisher.Abstraction` in the
`Application` project.
2026-07-28 10:49:32 +02:00
61c6e34b2d Add DigitalData.MessagingService.Publisher project
A new project, `DigitalData.MessagingService.Publisher`, has been added to the solution. The solution file (`DigitalData.MessagingService.sln`) was updated to include the project declaration, build configurations, and nesting under the appropriate parent project.

The new project targets `.NET 8.0` and includes the following configurations:
- Implicit Usings enabled.
- Nullable reference types enabled.
- Latest C# language version specified.
2026-07-28 10:40:55 +02:00
ac4682575c Enable implicit usings in project files
Added `<ImplicitUsings>` property with the value `enable` to both `DigitalData.MessagingService.Publisher.Abstraction.csproj` and `DigitalData.MessagingService.Client.DependencyInjection.csproj` to enable implicit global using directives. Adjusted the order of `<LangVersion>` to follow `<Nullable>` for consistency.
2026-07-28 10:40:21 +02:00
16 changed files with 267 additions and 29 deletions

View File

@@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingServic
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.MessagingService.Publisher", "src\infrastructure\DigitalData.MessagingService.Publisher\DigitalData.MessagingService.Publisher.csproj", "{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +77,10 @@ Global
{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
{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -91,6 +97,7 @@ Global
{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}
{8DBBAA7C-C4D3-4ADD-8372-B0D6260C8FFC} = {71BEA4D0-7835-4A8C-B11E-1088E0801DCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {90E29FDC-F6C6-414F-94BF-25DF61D18060}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Publisher.Abstraction\DigitalData.MessagingService.Publisher.Abstraction.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@ using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Queue;
using DigitalData.MessagingService.Infrastructure.Services;
using DigitalData.MessagingService.Infrastructure.Services.Background;
using DigitalData.MessagingService.Publisher.Abstraction;
using DigitalData.MessagingService.Publisher;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Configuration;
@@ -34,7 +34,7 @@ public static class DependencyInjection
// --- Email Queue (RabbitMQ) ---
services.AddSingleton<OutgoingEmailConsumer>();
services.AddSingleton<IOutgoingEmailPublisher, OutgoingEmailPublisher>();
services.AddMessagingServicePublisher();
// --- RabbitMQ Configuration ---
services.AddRabbitMqConnectionFactory(configuration);

View File

@@ -7,8 +7,9 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Publisher\DigitalData.MessagingService.Publisher.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.RabbitMQ\DigitalData.MessagingService.RabbitMQ.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,20 @@
using DigitalData.MessagingService.Publisher.Abstraction;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DigitalData.MessagingService.Publisher;
public static class DependencyInjection
{
public static IServiceCollection AddMessagingServicePublisher(this IServiceCollection services, Action<RabbitMqConfiguration>? configure = null)
{
if(configure is not null)
services.AddRabbitMqConnectionFactory(configure);
// --- Email Queue (RabbitMQ) ---
services.AddSingleton<IOutgoingEmailPublisher, OutgoingEmailPublisher>();
return services;
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<PackageReference Include="System.Text.Json" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Publisher.Abstraction\DigitalData.MessagingService.Publisher.Abstraction.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.RabbitMQ\DigitalData.MessagingService.RabbitMQ.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,7 +6,7 @@ using RabbitMQ.Client;
using DigitalData.MessagingService.RabbitMQ;
using DigitalData.MessagingService.Publisher.Abstraction;
namespace DigitalData.MessagingService.Infrastructure.Queue;
namespace DigitalData.MessagingService.Publisher;
/// <summary>
/// RabbitMQ-based email queue implementation for outgoing emails.

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace DigitalData.MessagingService.RabbitMQ
{
@@ -8,14 +9,31 @@ namespace DigitalData.MessagingService.RabbitMQ
/// </summary>
public static class DependencyInjection
{
private static IServiceCollection AddDefaultServices(this IServiceCollection services)
{
services.AddSingleton<RabbitMqConnectionFactory>();
return services;
}
/// <summary>
/// Adds Infrastructure layer services to the DI container
/// </summary>
public static IServiceCollection AddRabbitMqConnectionFactory(
this IServiceCollection services,
IConfiguration configuration)
public static IServiceCollection AddRabbitMqConnectionFactory(this IServiceCollection services, Action<RabbitMqConfiguration> configure)
{
services.AddSingleton<RabbitMqConnectionFactory>();
services.AddDefaultServices();
// --- RabbitMQ Configuration ---
services.Configure(configure);
return services;
}
/// <summary>
/// Adds Infrastructure layer services to the DI container
/// </summary>
public static IServiceCollection AddRabbitMqConnectionFactory(this IServiceCollection services, IConfiguration configuration)
{
services.AddDefaultServices();
// --- RabbitMQ Configuration ---
services.Configure<RabbitMqConfiguration>(
@@ -24,5 +42,4 @@ namespace DigitalData.MessagingService.RabbitMQ
return services;
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net8.0</TargetFrameworks>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@@ -45,11 +45,35 @@ namespace DigitalData.MessagingService.RabbitMQ
/// </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; }
/// <summary>
/// Name of the main queue where outbound email messages are consumed from.
/// </summary>
public string QueueName { get; set; } = "messaging-service.email.outbox";
/// <summary>
/// Name of the exchange to which email messages are published.
/// Messages are routed from this exchange to <see cref="QueueName"/> via <see cref="RoutingKey"/>.
/// </summary>
public string ExchangeName { get; set; } = "messaging-service.emails";
/// <summary>
/// Routing key used to bind <see cref="QueueName"/> to <see cref="ExchangeName"/>.
/// </summary>
public string RoutingKey { get; set; } = "email.outbox";
/// <summary>
/// Name of the Dead Letter Queue (DLQ) where messages that could not be processed are routed.
/// </summary>
public string DlqQueueName { get; set; } = "messaging-service.email.outbox.dlq";
/// <summary>
/// Name of the Dead Letter Exchange (DLX) that routes rejected or expired messages to <see cref="DlqQueueName"/>.
/// </summary>
public string DlqExchangeName { get; set; } = "messaging-service.emails.dlq";
/// <summary>
/// Routing key used to bind <see cref="DlqQueueName"/> to <see cref="DlqExchangeName"/>.
/// </summary>
public string DlqRoutingKey { get; set; } = "email.outbox.dlq";
}
}

View File

@@ -21,9 +21,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj" />
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\..\infrastructure\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -15,12 +15,14 @@
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Description></Description>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
</ItemGroup>
@@ -31,4 +33,9 @@
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\DigitalData.MessagingService.Publisher.Abstraction\DigitalData.MessagingService.Publisher.Abstraction.csproj" />
<ProjectReference Include="..\..\infrastructure\DigitalData.MessagingService.Publisher\DigitalData.MessagingService.Publisher.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,129 @@
using DigitalData.MessagingService.Publisher;
using DigitalData.MessagingService.Publisher.Abstraction;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace DigitalData.MessagingService.Client.DependencyInjection;
/// <summary>
/// Provides a static, self-contained client for sending emails via RabbitMQ
/// without requiring a host-level dependency injection container.
/// </summary>
/// <remarks>
/// This class manages its own internal <see cref="IServiceProvider"/> using a
/// <see cref="Lazy{T}"/> pattern so the DI container is only built once,
/// on the first call to <see cref="ConnectRabbitMq"/>.
/// </remarks>
public static class EmailSender
{
/// <summary>
/// Internal event used to accumulate service registrations before the
/// <see cref="IServiceProvider"/> is built. Handlers are added by
/// <see cref="ConnectRabbitMq"/> and invoked exactly once during
/// lazy initialization.
/// </summary>
private static event Action<IServiceCollection> ConfigureServices = delegate { };
/// <summary>
/// The lazily-initialized internal service provider.
/// Built on first access by invoking all registered
/// <see cref="ConfigureServices"/> handlers.
/// </summary>
private static readonly Lazy<IServiceProvider> LazyProvider = new(() =>
{
var services = new ServiceCollection();
ConfigureServices?.Invoke(services);
return services.BuildServiceProvider();
});
/// <summary>
/// Gets a value indicating whether the messaging service has been connected
/// and the internal <see cref="IServiceProvider"/> has been initialized.
/// </summary>
/// <value>
/// <see langword="true"/> if <see cref="ConnectRabbitMq"/> has been called
/// and the provider is built; otherwise <see langword="false"/>.
/// </value>
public static bool IsConnected => LazyProvider.IsValueCreated;
/// <summary>
/// Configures and establishes a connection to RabbitMQ, then initializes
/// the internal dependency injection container.
/// </summary>
/// <param name="configure">
/// A delegate used to configure the <see cref="RabbitMqConfiguration"/>,
/// such as host, port, credentials, and exchange settings.
/// </param>
/// <param name="onReconnect">
/// Controls the behavior when this method is called while already connected.
/// Defaults to <see cref="OnReconnect.ThrowException"/>.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown when the service is already connected and
/// <paramref name="onReconnect"/> is <see cref="OnReconnect.ThrowException"/>.
/// </exception>
public static void ConnectRabbitMq(Action<RabbitMqConfiguration> configure, OnReconnect onReconnect = OnReconnect.ThrowException)
{
if(IsConnected && onReconnect == OnReconnect.ThrowException)
throw new InvalidOperationException("Messaging service is already connected.");
ConfigureServices += services => services.AddMessagingServicePublisher(configure);
_ = LazyProvider.Value; // Force initialization
}
/// <summary>
/// Configures and establishes a connection to RabbitMQ using a URL, then initializes
/// the internal dependency injection container.
/// </summary>
/// <param name="url">
/// The RabbitMQ server URL (e.g. <c>amqp://hostname:5672/virtualhost</c>).
/// The host, port, and virtual host are extracted from this URL.
/// </param>
/// <param name="username">The username used to authenticate with RabbitMQ.</param>
/// <param name="password">The password used to authenticate with RabbitMQ.</param>
/// <param name="onReconnect">
/// Controls the behavior when this method is called while already connected.
/// Defaults to <see cref="OnReconnect.ThrowException"/>.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown when the service is already connected and
/// <paramref name="onReconnect"/> is <see cref="OnReconnect.ThrowException"/>.
/// </exception>
public static void ConnectRabbitMq(string url, string username, string password, OnReconnect onReconnect = OnReconnect.ThrowException)
{
var uri = new Uri(url);
ConnectRabbitMq(cfg =>
{
cfg.HostName = uri.Host;
cfg.Port = uri.IsDefaultPort ? 5672 : uri.Port;
cfg.UserName = username;
cfg.Password = password;
if (!string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
cfg.VirtualHost = Uri.UnescapeDataString(uri.AbsolutePath.TrimStart('/'));
}, onReconnect);
}
/// <summary>
/// Enqueues the specified email event to the RabbitMQ messaging pipeline.
/// </summary>
/// <param name="email">The outgoing email event to enqueue.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when <see cref="ConnectRabbitMq"/> has not been called prior to sending.
/// </exception>
/// <remarks>
/// This method resolves <see cref="IOutgoingEmailPublisher"/> from the internal
/// service provider and calls <c>EnqueueAsync</c> in a fire-and-forget manner.
/// Ensure that any unhandled exceptions from the async operation are handled
/// at the publisher level.
/// </remarks>
public static void Send(OutgoingEmailEvent email)
{
if(!IsConnected)
throw new InvalidOperationException("Messaging service is not connected. Call ConnectRabbitMq first.");
var publisher = LazyProvider.Value.GetRequiredService<IOutgoingEmailPublisher>();
publisher.EnqueueAsync(email);
}
}

View File

@@ -0,0 +1,20 @@
namespace DigitalData.MessagingService.Client.DependencyInjection;
/// <summary>
/// Defines the behavior when <see cref="EmailSender.ConnectRabbitMq"/> is called
/// while a connection has already been established.
/// </summary>
public enum OnReconnect
{
/// <summary>
/// Throws an <see cref="System.InvalidOperationException"/> if the messaging service
/// is already connected. This is the default behavior.
/// </summary>
ThrowException,
/// <summary>
/// Silently ignores the reconnection attempt if the messaging service
/// is already connected.
/// </summary>
Ignore
}

View File

@@ -16,13 +16,6 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DigitalData.MessagingService.API\DigitalData.MessagingService.API.csproj" />
<ProjectReference Include="..\..\src\DigitalData.MessagingService.Application\DigitalData.MessagingService.Application.csproj" />
<ProjectReference Include="..\..\src\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
<ProjectReference Include="..\..\src\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>