Compare commits
24 Commits
5e587da957
...
cd221e710e
| Author | SHA1 | Date | |
|---|---|---|---|
| cd221e710e | |||
| 78c82bf129 | |||
| 2ad2dc6b4d | |||
| 3149bb1cf9 | |||
| 0b3ff7cae9 | |||
| 977a20fd97 | |||
| 56ac720615 | |||
| becb608331 | |||
| 37009b8e1e | |||
| b2d478c335 | |||
| bf06b31656 | |||
| 77e7c796fa | |||
| 2d7af80cd3 | |||
| a2373f242a | |||
| 3611d527d4 | |||
| 885365df76 | |||
| 94d1b73c4a | |||
| 84c72af993 | |||
| 0eda732d49 | |||
| 42d35d9a01 | |||
| 23d52b9427 | |||
| 370872e126 | |||
| b6470fce5a | |||
| 1617d4ab43 |
@@ -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
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace DigitalData.MessagingService.Domain.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for aggregate roots in DDD
|
||||
/// </summary>
|
||||
public interface IAggregateRoot
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DigitalData.MessagingService.Domain.Enums;
|
||||
|
||||
public enum AttachmentStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Valid = 2,
|
||||
Corrupt = 3,
|
||||
Skipped = 4
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace DigitalData.MessagingService.Domain.Enums;
|
||||
|
||||
public enum AuthenticationType
|
||||
{
|
||||
UsernamePassword = 1,
|
||||
OAuth2 = 2
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace DigitalData.MessagingService.Domain.Enums;
|
||||
|
||||
public enum EmailStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Processing = 2,
|
||||
Processed = 3,
|
||||
Failed = 4,
|
||||
PartiallyProcessed = 5,
|
||||
Rejected = 6
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user