feat(client)!: unify client API around IMessagingServiceClient

MessagingServiceClient now implements IMessagingServiceClient and takes a PublishEmailViaSmtpCommand instead of the removed Email record. Add MessagingServiceHttpClient, which posts the same command to the API via IHttpClientFactory.

Replace DependencyInjectionExtensions with a single DependencyInjection entry point that registers exactly one client implementation and throws when a second one is added. It exposes AddMessagingServiceClient for RabbitMQ and AddMessagingServiceHttpClient for the HTTP API, both with an optional license key.

Bump the package to 2.0.0 because the SendAsync signatures and the removed Email type are breaking changes, add Microsoft.Extensions.Http, and update the integration test to exercise the new contract.
This commit is contained in:
2026-11-05 09:45:11 +01:00
parent c0e0fa3ba1
commit 3e86d4cfe2
7 changed files with 169 additions and 112 deletions

View File

@@ -0,0 +1,88 @@
#if NET
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.DependencyInjection;
using DigitalData.MessagingService.Publisher;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DigitalData.MessagingService.Client;
/// <summary>
///
/// </summary>
public static class DependencyInjection
{
private static IServiceCollection EnsureMessagingServiceClientNotRegistered(this IServiceCollection services)
{
if (services.Any(x => x.ServiceType == typeof(IMessagingServiceClient)))
throw new InvalidOperationException("IMessagingServiceClient is already registered. Register only one client implementation (MessagingServiceClient or MessagingServiceHttpClient).");
return services;
}
private static IServiceCollection AddMessagingServiceClientDefault(this IServiceCollection services, string? luckyPennySoftLicenseKey = null)
{
services.EnsureMessagingServiceClientNotRegistered();
services.AddMessagingServiceApplication(luckyPennySoftLicenseKey);
return services;
}
#region DI Extensions
/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <param name="luckyPennySoftLicenseKey"></param>
/// <param name="configureRabbitMqConnection"></param>
/// <returns></returns>
public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, Action<RabbitMqConnectionConfiguration> configureRabbitMqConnection, string? luckyPennySoftLicenseKey = null)
{
services.AddMessagingServiceClientDefault(luckyPennySoftLicenseKey);
services.AddMessagingServicePublisher(configureRabbitMqConnection);
services.AddTransient<IMessagingServiceClient, MessagingServiceClient>();
return services;
}
/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <param name="luckyPennySoftLicenseKey"></param>
/// <param name="configuration"></param>
/// <param name="rabbitMqConnectionSectionName"></param>
/// <returns></returns>
public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, IConfiguration configuration, string? luckyPennySoftLicenseKey = null, string rabbitMqConnectionSectionName = RabbitMqConnectionConfiguration.SectionName)
{
services.AddMessagingServiceClientDefault(luckyPennySoftLicenseKey);
services.AddMessagingServicePublisher(configuration, rabbitMqConnectionSectionName);
services.AddTransient<IMessagingServiceClient, MessagingServiceClient>();
return services;
}
/// <summary>
/// Registers <see cref="MessagingServiceHttpClient"/> as <see cref="IMessagingServiceClient"/>.
/// Configure <see cref="HttpClient.BaseAddress"/> for the named client <c>MessagingServiceHttpClient</c>.
/// </summary>
public static IServiceCollection AddMessagingServiceHttpClient(this IServiceCollection services, Action<HttpClient> configureHttpClient)
{
services.EnsureMessagingServiceClientNotRegistered();
services.AddHttpClient(nameof(MessagingServiceHttpClient), configureHttpClient);
services.AddTransient<IMessagingServiceClient, MessagingServiceHttpClient>();
return services;
}
/// <summary>
/// Registers <see cref="MessagingServiceHttpClient"/> as <see cref="IMessagingServiceClient"/>.
/// Configure <see cref="HttpClient.BaseAddress"/> for the named client <c>MessagingServiceHttpClient</c> using the provided base address.
/// </summary>
/// <param name="services"></param>
/// <param name="baseAddress"></param>
/// <returns></returns>
public static IServiceCollection AddMessagingServiceHttpClient(IServiceCollection services, string baseAddress)
{
// do not call EnsureMessagingServiceClientNotRegistered here because AddMessagingServiceHttpClient is already calling it in the overload above
return services.AddMessagingServiceHttpClient(client => client.BaseAddress = new Uri(baseAddress));
}
#endregion DI Extensions
}
#endif

View File

@@ -1,50 +0,0 @@
#if NET
using DigitalData.MessagingService.Application.DependencyInjection;
using DigitalData.MessagingService.Publisher;
using DigitalData.MessagingService.RabbitMQ;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DigitalData.MessagingService.Client.DependencyInjection;
/// <summary>
///
/// </summary>
public static class DependencyInjectionExtensions
{
private static IServiceCollection AddMessagingServiceClientDefaults(this IServiceCollection services, string luckyPennySoftLicenseKey)
{
services.AddMessagingServiceApplication(luckyPennySoftLicenseKey);
return services.AddTransient<MessagingServiceClient>();
}
/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <param name="luckyPennySoftLicenseKey"></param>
/// <param name="configureRabbitMqConnection"></param>
/// <returns></returns>
public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, string luckyPennySoftLicenseKey, Action<RabbitMqConnectionConfiguration> configureRabbitMqConnection)
{
services.AddMessagingServiceClientDefaults(luckyPennySoftLicenseKey);
services.AddMessagingServicePublisher(configureRabbitMqConnection);
return services;
}
/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <param name="luckyPennySoftLicenseKey"></param>
/// <param name="configuration"></param>
/// <param name="rabbitMqConnectionSectionName"></param>
/// <returns></returns>
public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, string luckyPennySoftLicenseKey, IConfiguration configuration, string rabbitMqConnectionSectionName = RabbitMqConnectionConfiguration.SectionName)
{
services.AddMessagingServiceClientDefaults(luckyPennySoftLicenseKey);
services.AddMessagingServicePublisher(configuration, rabbitMqConnectionSectionName);
return services;
}
}
#endif

View File

@@ -11,9 +11,9 @@
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
<PackageTags>digital data messaging service api client</PackageTags>
<Version>1.0.5</Version>
<AssemblyVersion>1.0.5.0</AssemblyVersion>
<FileVersion>1.0.5.0</FileVersion>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<Description></Description>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -27,6 +27,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.10" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">

View File

@@ -1,34 +0,0 @@
#if NET
namespace DigitalData.MessagingService.Client;
/// <summary>
/// Represents an email to be sent via the messaging service.
/// </summary>
public record Email
{
/// <summary>
/// Sender email address.
/// </summary>
public required string From { 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>
/// Indicates whether <see cref="Body"/> is HTML.
/// </summary>
public required bool IsHtml { get; init; }
/// <summary>
/// Recipient email addresses.
/// </summary>
public required List<string> To { get; init; }
}
#endif

View File

@@ -1,5 +1,5 @@
#if NET
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using MediatR;
@@ -8,27 +8,17 @@ namespace DigitalData.MessagingService.Client;
/// <summary>
///
/// </summary>
public class MessagingServiceClient(ISender sender)
public class MessagingServiceClient(ISender sender) : IMessagingServiceClient
{
/// <summary>
///
/// </summary>
/// <param name="email"></param>
/// <param name="command"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task SendAsync(Email email, CancellationToken cancellationToken = default)
public Task<Guid> SendAsync(PublishEmailViaSmtpCommand command, CancellationToken cancellationToken = default)
{
return sender.Send(new PublishEmailViaSmtpCommand()
{
Sender = new GetEmailAccountQuery()
{
Username = email.From
},
Recipients = email.To,
Subject = email.Subject,
Body = email.Body,
IsHtml = email.IsHtml
}, cancellationToken);
return sender.Send(command, cancellationToken);
}
}
#endif
#endif

View File

@@ -0,0 +1,57 @@
#if NET
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using System.Net.Http.Json;
namespace DigitalData.MessagingService.Client;
/// <summary>
///
/// </summary>
public class MessagingServiceHttpClient(IHttpClientFactory httpClientFactory) : IMessagingServiceClient
{
private sealed record SendEmailAcceptedResponse(Guid Id);
/// <summary>
///
/// </summary>
/// <param name="command"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="HttpRequestException"></exception>
public async Task<Guid> SendAsync(PublishEmailViaSmtpCommand command, CancellationToken cancellationToken = default)
{
var httpClient = httpClientFactory.CreateClient(nameof(MessagingServiceHttpClient));
var senderUsernameField = $"{nameof(PublishEmailViaSmtpCommand.Sender)}.{nameof(GetEmailAccountQuery.Username)}";
using var form = new MultipartFormDataContent
{
{ new StringContent(command.Sender.Username ?? string.Empty), senderUsernameField },
{ new StringContent(command.Subject), nameof(PublishEmailViaSmtpCommand.Subject) },
{ new StringContent(command.Body), nameof(PublishEmailViaSmtpCommand.Body) },
{ new StringContent(command.IsHtml.ToString().ToLowerInvariant()), nameof(PublishEmailViaSmtpCommand.IsHtml) }
};
foreach (var recipient in command.Recipients)
form.Add(new StringContent(recipient), nameof(PublishEmailViaSmtpCommand.Recipients));
using var response = await httpClient.PostAsync("api/Email/smtp", form, cancellationToken);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<SendEmailAcceptedResponse>(cancellationToken: cancellationToken)
?? throw new HttpRequestException("MessagingService API response body is empty.");
return payload.Id;
}
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
throw new HttpRequestException(
$"MessagingService API request failed with status {(int)response.StatusCode} ({response.StatusCode}). Body: {responseText}",
null,
response.StatusCode);
}
}
#endif

View File

@@ -2,9 +2,11 @@ using System.Linq.Expressions;
using System.Text;
using System.Text.Json;
using DigitalData.MessagingService.Application.Common.Dto;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using DigitalData.MessagingService.Client;
using DigitalData.MessagingService.Client.DependencyInjection;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Publisher;
using DigitalData.MessagingService.RabbitMQ;
@@ -18,7 +20,7 @@ public sealed class MessagingServiceClientTests : IAsyncDisposable
private const string RecipientAddress = "htek0100@gmail.com";
private readonly ServiceProvider _serviceProvider;
private readonly MessagingServiceClient _client;
private readonly IMessagingServiceClient _client;
private readonly RabbitMqConnectionFactory _factory;
public MessagingServiceClientTests()
@@ -26,7 +28,7 @@ public sealed class MessagingServiceClientTests : IAsyncDisposable
var services = new ServiceCollection();
services.AddLogging();
services.AddMessagingServiceClient(LoadLuckyPennySoftLicenseKey(), RabbitMqTestConfig.Apply);
services.AddMessagingServiceClient(RabbitMqTestConfig.Apply, LoadLuckyPennySoftLicenseKey());
services.AddSingleton<IRepository<EmailAccount>>(
new InMemoryEmailAccountRepository(
[
@@ -43,12 +45,12 @@ public sealed class MessagingServiceClientTests : IAsyncDisposable
]));
_serviceProvider = services.BuildServiceProvider();
_client = _serviceProvider.GetRequiredService<MessagingServiceClient>();
_client = _serviceProvider.GetRequiredService<IMessagingServiceClient>();
_factory = _serviceProvider.GetRequiredService<RabbitMqConnectionFactory>();
}
/// <summary>
/// Sends an email through <see cref="MessagingServiceClient"/> using the real RabbitMQ setup.
/// Sends an email through <see cref="IMessagingServiceClient"/> using the real RabbitMQ setup.
/// This test intentionally uses manual mailbox verification because the shared queue can be
/// consumed by other services during test execution.
/// </summary>
@@ -57,16 +59,19 @@ public sealed class MessagingServiceClientTests : IAsyncDisposable
{
var subject = $"MessagingServiceClient Integration Test {Guid.NewGuid():N}";
var email = new Email
var command = new PublishEmailViaSmtpCommand
{
From = SenderAddress,
To = [RecipientAddress],
Sender = new GetEmailAccountQuery
{
Username = SenderAddress
},
Recipients = [RecipientAddress],
Subject = subject,
Body = "<p>Realistic test mail via MessagingServiceClient.</p>",
IsHtml = true
};
await _client.SendAsync(email);
await _client.SendAsync(command);
Assert.True(true, "Verify in mailbox.");
}