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."); }