From 3e86d4cfe251933dfce7c841ac8074d492f80740 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 5 Nov 2026 09:45:11 +0100 Subject: [PATCH] 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. --- .../DependencyInjection.cs | 88 +++++++++++++++++++ .../DependencyInjectionExtensions.cs | 50 ----------- ...DigitalData.MessagingService.Client.csproj | 7 +- .../Email.cs | 34 ------- .../MessagingServiceClient.cs | 22 ++--- .../MessagingServiceHttpClient.cs | 57 ++++++++++++ .../MessagingServiceClientTests.cs | 23 +++-- 7 files changed, 169 insertions(+), 112 deletions(-) create mode 100644 src/presentation/DigitalData.MessagingService.Client/DependencyInjection.cs delete mode 100644 src/presentation/DigitalData.MessagingService.Client/DependencyInjectionExtensions.cs delete mode 100644 src/presentation/DigitalData.MessagingService.Client/Email.cs create mode 100644 src/presentation/DigitalData.MessagingService.Client/MessagingServiceHttpClient.cs diff --git a/src/presentation/DigitalData.MessagingService.Client/DependencyInjection.cs b/src/presentation/DigitalData.MessagingService.Client/DependencyInjection.cs new file mode 100644 index 0000000..ea6d6c0 --- /dev/null +++ b/src/presentation/DigitalData.MessagingService.Client/DependencyInjection.cs @@ -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; + +/// +/// +/// +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 + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, Action configureRabbitMqConnection, string? luckyPennySoftLicenseKey = null) + { + services.AddMessagingServiceClientDefault(luckyPennySoftLicenseKey); + services.AddMessagingServicePublisher(configureRabbitMqConnection); + services.AddTransient(); + return services; + } + + /// + /// + /// + /// + /// + /// + /// + /// + 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(); + return services; + } + + /// + /// Registers as . + /// Configure for the named client MessagingServiceHttpClient. + /// + public static IServiceCollection AddMessagingServiceHttpClient(this IServiceCollection services, Action configureHttpClient) + { + services.EnsureMessagingServiceClientNotRegistered(); + services.AddHttpClient(nameof(MessagingServiceHttpClient), configureHttpClient); + services.AddTransient(); + return services; + } + + /// + /// Registers as . + /// Configure for the named client MessagingServiceHttpClient using the provided base address. + /// + /// + /// + /// + 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 diff --git a/src/presentation/DigitalData.MessagingService.Client/DependencyInjectionExtensions.cs b/src/presentation/DigitalData.MessagingService.Client/DependencyInjectionExtensions.cs deleted file mode 100644 index 1ab7182..0000000 --- a/src/presentation/DigitalData.MessagingService.Client/DependencyInjectionExtensions.cs +++ /dev/null @@ -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; - -/// -/// -/// -public static class DependencyInjectionExtensions -{ - private static IServiceCollection AddMessagingServiceClientDefaults(this IServiceCollection services, string luckyPennySoftLicenseKey) - { - services.AddMessagingServiceApplication(luckyPennySoftLicenseKey); - return services.AddTransient(); - } - - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddMessagingServiceClient(this IServiceCollection services, string luckyPennySoftLicenseKey, Action configureRabbitMqConnection) - { - services.AddMessagingServiceClientDefaults(luckyPennySoftLicenseKey); - services.AddMessagingServicePublisher(configureRabbitMqConnection); - return services; - } - - /// - /// - /// - /// - /// - /// - /// - /// - 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 \ No newline at end of file diff --git a/src/presentation/DigitalData.MessagingService.Client/DigitalData.MessagingService.Client.csproj b/src/presentation/DigitalData.MessagingService.Client/DigitalData.MessagingService.Client.csproj index 03cfacd..d2a5af4 100644 --- a/src/presentation/DigitalData.MessagingService.Client/DigitalData.MessagingService.Client.csproj +++ b/src/presentation/DigitalData.MessagingService.Client/DigitalData.MessagingService.Client.csproj @@ -11,9 +11,9 @@ icon.png http://git.dd:3000/AppStd/Rec.git digital data messaging service api client - 1.0.5 - 1.0.5.0 - 1.0.5.0 + 2.0.0 + 2.0.0.0 + 2.0.0.0 enable enable @@ -27,6 +27,7 @@ + diff --git a/src/presentation/DigitalData.MessagingService.Client/Email.cs b/src/presentation/DigitalData.MessagingService.Client/Email.cs deleted file mode 100644 index 68e48db..0000000 --- a/src/presentation/DigitalData.MessagingService.Client/Email.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if NET -namespace DigitalData.MessagingService.Client; - -/// -/// Represents an email to be sent via the messaging service. -/// -public record Email -{ - /// - /// Sender email address. - /// - public required string From { get; init; } - - /// - /// Email subject. - /// - public required string Subject { get; init; } - - /// - /// Email body (HTML or plain text). - /// - public required string Body { get; init; } - - /// - /// Indicates whether is HTML. - /// - public required bool IsHtml { get; init; } - - /// - /// Recipient email addresses. - /// - public required List To { get; init; } -} -#endif diff --git a/src/presentation/DigitalData.MessagingService.Client/MessagingServiceClient.cs b/src/presentation/DigitalData.MessagingService.Client/MessagingServiceClient.cs index 86fd481..6d960ff 100644 --- a/src/presentation/DigitalData.MessagingService.Client/MessagingServiceClient.cs +++ b/src/presentation/DigitalData.MessagingService.Client/MessagingServiceClient.cs @@ -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; /// /// /// -public class MessagingServiceClient(ISender sender) +public class MessagingServiceClient(ISender sender) : IMessagingServiceClient { /// /// /// - /// + /// /// /// - public Task SendAsync(Email email, CancellationToken cancellationToken = default) + public Task 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 \ No newline at end of file +#endif diff --git a/src/presentation/DigitalData.MessagingService.Client/MessagingServiceHttpClient.cs b/src/presentation/DigitalData.MessagingService.Client/MessagingServiceHttpClient.cs new file mode 100644 index 0000000..7f3ee13 --- /dev/null +++ b/src/presentation/DigitalData.MessagingService.Client/MessagingServiceHttpClient.cs @@ -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; + +/// +/// +/// +public class MessagingServiceHttpClient(IHttpClientFactory httpClientFactory) : IMessagingServiceClient +{ + private sealed record SendEmailAcceptedResponse(Guid Id); + + /// + /// + /// + /// + /// + /// + /// + public async Task 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(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 diff --git a/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs b/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs index ae7ef62..f741c67 100644 --- a/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs +++ b/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs @@ -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>( new InMemoryEmailAccountRepository( [ @@ -43,12 +45,12 @@ public sealed class MessagingServiceClientTests : IAsyncDisposable ])); _serviceProvider = services.BuildServiceProvider(); - _client = _serviceProvider.GetRequiredService(); + _client = _serviceProvider.GetRequiredService(); _factory = _serviceProvider.GetRequiredService(); } /// - /// Sends an email through using the real RabbitMQ setup. + /// Sends an email through 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. /// @@ -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 = "

Realistic test mail via MessagingServiceClient.

", IsHtml = true }; - await _client.SendAsync(email); + await _client.SendAsync(command); Assert.True(true, "Verify in mailbox."); }