diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Common/ConfigurationReaderConnectionStringRepository.cs b/MessagingService.BusinessLogic/Common/ConfigurationReaderConnectionStringRepository.cs new file mode 100644 index 0000000..f02dea5 --- /dev/null +++ b/MessagingService.BusinessLogic/Common/ConfigurationReaderConnectionStringRepository.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MessagingService.BusinessLogic.Common +{ + using System.Data.Common; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Data.SqlClient; + using Shared.General; + using Shared.Repositories; + + [ExcludeFromCodeCoverage] + public class ConfigurationReaderConnectionStringRepository : IConnectionStringConfigurationRepository + { + #region Methods + + /// + /// Creates the connection string. + /// + /// The external identifier. + /// Type of the connection string. + /// The connection string. + /// The cancellation token. + public async Task CreateConnectionString(String externalIdentifier, + ConnectionStringType connectionStringType, + String connectionString, + CancellationToken cancellationToken) + { + throw new NotImplementedException("This is only required to complete the interface"); + } + + /// + /// Deletes the connection string configuration. + /// + /// The external identifier. + /// Type of the connection string. + /// The cancellation token. + public async Task DeleteConnectionStringConfiguration(String externalIdentifier, + ConnectionStringType connectionStringType, + CancellationToken cancellationToken) + { + throw new NotImplementedException("This is only required to complete the interface"); + } + + /// + /// Gets the connection string. + /// + /// The external identifier. + /// Type of the connection string. + /// The cancellation token. + /// + public async Task GetConnectionString(String externalIdentifier, + ConnectionStringType connectionStringType, + CancellationToken cancellationToken) + { + String connectionString = string.Empty; + String databaseName = string.Empty; + switch (connectionStringType) + { + case ConnectionStringType.ReadModel: + databaseName = "EstateReportingReadModel" + externalIdentifier; + connectionString = ConfigurationReader.GetConnectionString("EstateReportingReadModel"); + break; + default: + throw new NotSupportedException($"Connection String type [{connectionStringType}] is not supported"); + } + + DbConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString) + { + InitialCatalog = databaseName + }; + + return builder.ToString(); + } + + #endregion + } +} diff --git a/MessagingService.BusinessLogic/MessagingService.BusinessLogic.csproj b/MessagingService.BusinessLogic/MessagingService.BusinessLogic.csproj new file mode 100644 index 0000000..ba572f1 --- /dev/null +++ b/MessagingService.BusinessLogic/MessagingService.BusinessLogic.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + diff --git a/MessagingService.BusinessLogic/RequestHandlers/EmailRequestHandler.cs b/MessagingService.BusinessLogic/RequestHandlers/EmailRequestHandler.cs new file mode 100644 index 0000000..f5efb00 --- /dev/null +++ b/MessagingService.BusinessLogic/RequestHandlers/EmailRequestHandler.cs @@ -0,0 +1,65 @@ +namespace MessagingService.BusinessLogic.RequestHandlers +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using MediatR; + using Requests; + using Services; + + /// + /// + /// + /// + public class EmailRequestHandler : IRequestHandler + { + #region Fields + + /// + /// The email domain service + /// + private readonly IEmailDomainService EmailDomainService; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The email domain service. + public EmailRequestHandler(IEmailDomainService emailDomainService) + { + this.EmailDomainService = emailDomainService; + } + + #endregion + + #region Methods + + /// + /// Handles a request + /// + /// The request + /// Cancellation token + /// + /// Response from the request + /// + public async Task Handle(SendEmailRequest request, + CancellationToken cancellationToken) + { + await this.EmailDomainService.SendEmailMessage(request.ConnectionIdentifier, + request.MessageId, + request.FromAddress, + request.ToAddresses, + request.Subject, + request.Body, + request.IsHtml, + cancellationToken); + + return string.Empty; + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Requests/SendEmailRequest.cs b/MessagingService.BusinessLogic/Requests/SendEmailRequest.cs new file mode 100644 index 0000000..95a0a00 --- /dev/null +++ b/MessagingService.BusinessLogic/Requests/SendEmailRequest.cs @@ -0,0 +1,130 @@ +namespace MessagingService.BusinessLogic.Requests +{ + using System; + using System.Collections.Generic; + using MediatR; + + /// + /// + /// + /// + public class SendEmailRequest : IRequest + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The connection identifier. + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + private SendEmailRequest(Guid connectionIdentifier, + Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml) + { + this.ConnectionIdentifier = connectionIdentifier; + this.MessageId = messageId; + this.FromAddress = fromAddress; + this.ToAddresses = toAddresses; + this.Subject = subject; + this.Body = body; + this.IsHtml = isHtml; + } + + #endregion + + #region Properties + + /// + /// Gets the body. + /// + /// + /// The body. + /// + public String Body { get; } + + /// + /// Gets the connection identifier. + /// + /// + /// The connection identifier. + /// + public Guid ConnectionIdentifier { get; } + + /// + /// Gets from address. + /// + /// + /// From address. + /// + public String FromAddress { get; } + + /// + /// Gets a value indicating whether this instance is HTML. + /// + /// + /// true if this instance is HTML; otherwise, false. + /// + public Boolean IsHtml { get; } + + /// + /// Gets the message identifier. + /// + /// + /// The message identifier. + /// + public Guid MessageId { get; } + + /// + /// Gets the subject. + /// + /// + /// The subject. + /// + public String Subject { get; } + + /// + /// Converts to address. + /// + /// + /// To address. + /// + public List ToAddresses { get; } + + #endregion + + #region Methods + + /// + /// Creates the specified from address. + /// + /// The connection identifier. + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// + public static SendEmailRequest Create(Guid connectionIdentifier, + Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml) + { + return new SendEmailRequest(connectionIdentifier, messageId, fromAddress, toAddresses, subject, body, isHtml); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailDomainService.cs b/MessagingService.BusinessLogic/Services/EmailDomainService.cs new file mode 100644 index 0000000..41eb16c --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailDomainService.cs @@ -0,0 +1,91 @@ +namespace MessagingService.BusinessLogic.Services +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using EmailMessageAggregate; + using EmailServices; + using Shared.DomainDrivenDesign.EventStore; + using Shared.EventStore.EventStore; + + /// + /// + /// + /// + public class EmailDomainService : IEmailDomainService + { + #region Fields + + /// + /// The aggregate repository manager + /// + private readonly IAggregateRepositoryManager AggregateRepositoryManager; + + /// + /// The email service proxy + /// + private readonly IEmailServiceProxy EmailServiceProxy; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate repository manager. + /// The email service proxy. + public EmailDomainService(IAggregateRepositoryManager aggregateRepositoryManager, + IEmailServiceProxy emailServiceProxy) + { + this.AggregateRepositoryManager = aggregateRepositoryManager; + this.EmailServiceProxy = emailServiceProxy; + } + + #endregion + + #region Methods + + /// + /// Sends the email message. + /// + /// The connection identifier. + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// The cancellation token. + public async Task SendEmailMessage(Guid connectionIdentifier, + Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml, + CancellationToken cancellationToken) + { + IAggregateRepository emailAggregateRepository = this.AggregateRepositoryManager.GetAggregateRepository(connectionIdentifier); + + // Rehydrate Email Message aggregate + EmailAggregate emailAggregate = await emailAggregateRepository.GetLatestVersion(messageId, cancellationToken); + + // send message to provider (record event) + emailAggregate.SendRequestToProvider(fromAddress, toAddresses, subject, body, isHtml); + + // Make call to Email provider here + EmailServiceProxyResponse emailResponse = + await this.EmailServiceProxy.SendEmail(messageId, fromAddress, toAddresses, subject, body, isHtml, cancellationToken); + + // response message from provider (record event) + emailAggregate.ReceiveResponseFromProvider(emailResponse.RequestIdentifier, emailResponse.EmailIdentifier); + + // Save Changes to persistance + await emailAggregateRepository.SaveChanges(emailAggregate, cancellationToken); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/EmailServiceProxyResponse.cs b/MessagingService.BusinessLogic/Services/EmailServices/EmailServiceProxyResponse.cs new file mode 100644 index 0000000..516d308 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/EmailServiceProxyResponse.cs @@ -0,0 +1,51 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices +{ + using System; + using System.Net; + + /// + /// + /// + public class EmailServiceProxyResponse + { + /// + /// Gets or sets the API status code. + /// + /// + /// The API status code. + /// + public HttpStatusCode ApiStatusCode { get; set; } + + /// + /// Gets or sets the request identifier. + /// + /// + /// The request identifier. + /// + public String RequestIdentifier { get; set; } + + /// + /// Gets or sets the email identifier. + /// + /// + /// The email identifier. + /// + public String EmailIdentifier { get; set; } + + /// + /// Gets or sets the error code. + /// + /// + /// The error code. + /// + public String ErrorCode { get; set; } + + /// + /// Gets or sets the error. + /// + /// + /// The error. + /// + public String Error { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs new file mode 100644 index 0000000..3a8780e --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/IEmailServiceProxy.cs @@ -0,0 +1,33 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public interface IEmailServiceProxy + { + #region Methods + + /// + /// Sends the email. + /// + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// The cancellation token. + /// + Task SendEmail(Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml, + CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs new file mode 100644 index 0000000..8036e5d --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/IntegrationTest/IntegrationTestEmailServiceProxy.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MessagingService.Service.Services.Email.IntegrationTest +{ + using BusinessLogic.Requests; + using BusinessLogic.Services.EmailServices; + + public class IntegrationTestEmailServiceProxy : IEmailServiceProxy + { + /// + /// Sends the email. + /// + /// The request. + /// The cancellation token. + /// + public async Task SendEmail(Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml, CancellationToken cancellationToken) + { + return new EmailServiceProxyResponse + { + RequestIdentifier = "requestid", + EmailIdentifier = "emailid", + ApiStatusCode = HttpStatusCode.OK, + Error = String.Empty, + ErrorCode = String.Empty + }; + } + } +} diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs new file mode 100644 index 0000000..9aa4432 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoProxy.cs @@ -0,0 +1,103 @@ +namespace MessagingService.BusinessLogic.Services.EmailServices.Smtp2Go +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Newtonsoft.Json; + using Service.Services.Email.Smtp2Go; + using Shared.General; + + /// + /// + /// + /// + public class Smtp2GoProxy : IEmailServiceProxy + { + #region Fields + + /// + /// The HTTP client + /// + private readonly HttpClient HttpClient; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + public Smtp2GoProxy(HttpClient httpClient) + { + this.HttpClient = httpClient; + } + + #endregion + + #region Methods + + /// + /// Sends the email. + /// + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// The cancellation token. + /// + public async Task SendEmail(Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml, + CancellationToken cancellationToken) + { + EmailServiceProxyResponse response = null; + + // Translate the request message + Smtp2GoSendEmailRequest apiRequest = new Smtp2GoSendEmailRequest + { + ApiKey = ConfigurationReader.GetValue("SMTP2GoAPIKey"), + HTMLBody = isHtml ? body : string.Empty, + TextBody = isHtml ? string.Empty : body, + Sender = fromAddress, + Subject = subject, + TestMode = false, + To = toAddresses.ToArray() + }; + + String requestSerialised = JsonConvert.SerializeObject(apiRequest); + StringContent content = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + using(HttpClient client = new HttpClient()) + { + client.BaseAddress = new Uri(ConfigurationReader.GetValue("SMTP2GoBaseAddress")); + + HttpResponseMessage httpResponse = await client.PostAsync("email/send", content, cancellationToken); + + Smtp2GoSendEmailResponse apiResponse = JsonConvert.DeserializeObject(await httpResponse.Content.ReadAsStringAsync()); + + // Translate the Response + response = new EmailServiceProxyResponse + { + ApiStatusCode = httpResponse.StatusCode, + EmailIdentifier = apiResponse.Data.EmailId, + Error = apiResponse.Data.Error, + ErrorCode = apiResponse.Data.ErrorCode, + RequestIdentifier = apiResponse.RequestId + }; + } + + return response; + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailRequest.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailRequest.cs new file mode 100644 index 0000000..4fbc2c5 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailRequest.cs @@ -0,0 +1,89 @@ +using System; +using Newtonsoft.Json; + +namespace MessagingService.Service.Services.Email.Smtp2Go +{ + public class Smtp2GoSendEmailRequest + { + /// + /// Gets or sets the API key. + /// + /// + /// The API key. + /// + [JsonProperty("api_key")] + public String ApiKey { get; set; } + + /// + /// Gets or sets a value indicating whether [test mode]. + /// + /// + /// true if [test mode]; otherwise, false. + /// + [JsonProperty("test")] + public Boolean TestMode { get; set; } + + /// + /// Gets or sets the sender. + /// + /// + /// The sender. + /// + [JsonProperty("sender")] + public String Sender { get; set; } + + /// + /// Gets or sets to. + /// + /// + /// To. + /// + [JsonProperty("to")] + public String[] To { get; set; } + + /// + /// Gets or sets the cc. + /// + /// + /// The cc. + /// + [JsonProperty("cc")] + public String[] CC { get; set; } + + /// + /// Gets or sets the BCC. + /// + /// + /// The BCC. + /// + [JsonProperty("bcc")] + public String[] BCC { get; set; } + + /// + /// Gets or sets the subject. + /// + /// + /// The subject. + /// + [JsonProperty("subject")] + public String Subject { get; set; } + + /// + /// Gets or sets the HTML body. + /// + /// + /// The HTML body. + /// + [JsonProperty("html_body")] + public String HTMLBody { get; set; } + + /// + /// Gets or sets the text body. + /// + /// + /// The text body. + /// + [JsonProperty("text_body")] + public String TextBody { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponse.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponse.cs new file mode 100644 index 0000000..a7af089 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponse.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace MessagingService.Service.Services.Email.Smtp2Go +{ + public class Smtp2GoSendEmailResponse + { + /// + /// Gets or sets the request identifier. + /// + /// + /// The request identifier. + /// + [JsonProperty("request_id")] + public String RequestId { get; set; } + + /// + /// Gets or sets the data. + /// + /// + /// The data. + /// + [JsonProperty("data")] + public Smtp2GoSendEmailResponseData Data { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponseData.cs b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponseData.cs new file mode 100644 index 0000000..988ef28 --- /dev/null +++ b/MessagingService.BusinessLogic/Services/EmailServices/Smtp2Go/Smtp2GoSendEmailResponseData.cs @@ -0,0 +1,62 @@ +using System; +using Newtonsoft.Json; + +namespace MessagingService.Service.Services.Email.Smtp2Go +{ + public class Smtp2GoSendEmailResponseData + { + /// + /// Gets or sets the failed. + /// + /// + /// The failed. + /// + [JsonProperty("failed")] + public Int32 Failed { get; set; } + + /// + /// Gets or sets the failures. + /// + /// + /// The failures. + /// + [JsonProperty("failures")] + public String[] Failures { get; set; } + + /// + /// Gets or sets the email identifier. + /// + /// + /// The email identifier. + /// + [JsonProperty("email_id")] + public String EmailId { get; set; } + + /// + /// Gets or sets the succeesful. + /// + /// + /// The succeesful. + /// + [JsonProperty("succeeded")] + public Int32 Succeesful { get; set; } + + /// + /// Gets or sets the error. + /// + /// + /// The error. + /// + [JsonProperty("error")] + public String Error { get; set; } + + /// + /// Gets or sets the error code. + /// + /// + /// The error code. + /// + [JsonProperty("error_code")] + public String ErrorCode { get; set; } + } +} \ No newline at end of file diff --git a/MessagingService.BusinessLogic/Services/IEmailDomainService.cs b/MessagingService.BusinessLogic/Services/IEmailDomainService.cs new file mode 100644 index 0000000..d16838b --- /dev/null +++ b/MessagingService.BusinessLogic/Services/IEmailDomainService.cs @@ -0,0 +1,38 @@ +namespace MessagingService.BusinessLogic.Services +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + /// + /// + /// + public interface IEmailDomainService + { + #region Methods + + /// + /// Sends the email message. + /// + /// The connection identifier. + /// The message identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// The cancellation token. + /// + Task SendEmailMessage(Guid connectionIdentifier, + Guid messageId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml, + CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj b/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/MessagingService.DataTransferObjects/SendEmailRequest.cs b/MessagingService.DataTransferObjects/SendEmailRequest.cs new file mode 100644 index 0000000..cec5f51 --- /dev/null +++ b/MessagingService.DataTransferObjects/SendEmailRequest.cs @@ -0,0 +1,60 @@ +namespace MessagingService.DataTransferObjects +{ + using System; + using System.Collections.Generic; + + public class SendEmailRequest + { + #region Properties + + /// + /// Gets or sets the body. + /// + /// + /// The body. + /// + public String Body { get; set; } + + /// + /// Gets or sets the connection identifier. + /// + /// + /// The connection identifier. + /// + public Guid ConnectionIdentifier { get; set; } + + /// + /// Gets or sets from address. + /// + /// + /// From address. + /// + public String FromAddress { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is HTML. + /// + /// + /// true if this instance is HTML; otherwise, false. + /// + public Boolean IsHtml { get; set; } + + /// + /// Gets or sets the subject. + /// + /// + /// The subject. + /// + public String Subject { get; set; } + + /// + /// Gets or sets to addresses. + /// + /// + /// To addresses. + /// + public List ToAddresses { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.DataTransferObjects/SendEmailResponse.cs b/MessagingService.DataTransferObjects/SendEmailResponse.cs new file mode 100644 index 0000000..6815675 --- /dev/null +++ b/MessagingService.DataTransferObjects/SendEmailResponse.cs @@ -0,0 +1,22 @@ +namespace MessagingService.DataTransferObjects +{ + using System; + + /// + /// + /// + public class SendEmailResponse + { + #region Properties + + /// + /// Gets or sets the message identifier. + /// + /// + /// The message identifier. + /// + public Guid MessageId { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessageAggregate/EmailAggregate.cs b/MessagingService.EmailMessageAggregate/EmailAggregate.cs new file mode 100644 index 0000000..0a77798 --- /dev/null +++ b/MessagingService.EmailMessageAggregate/EmailAggregate.cs @@ -0,0 +1,158 @@ +namespace MessagingService.EmailMessageAggregate +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using EmailMessage.DomainEvents; + using Microsoft.EntityFrameworkCore.Migrations.Operations; + using Shared.DomainDrivenDesign.EventSourcing; + using Shared.DomainDrivenDesign.EventStore; + using Shared.General; + + /// + /// + /// + /// + public class EmailAggregate : Aggregate + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public EmailAggregate() + { + this.Recipients = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + private EmailAggregate(Guid aggregateId) + { + Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); + + this.AggregateId = aggregateId; + this.Recipients = new List(); + } + + #endregion + + #region Methods + + /// + /// Messages the send to recipient failure. + /// + public void MessageSendToRecipientFailure() + { + } + + /// + /// Messages the send to recipient successful. + /// + public void MessageSendToRecipientSuccessful() + { + } + + /// + /// Receives the response from provider. + /// + /// The provider request reference. + /// The provider email reference. + public void ReceiveResponseFromProvider(String providerRequestReference, + String providerEmailReference) + { + ResponseReceivedFromProviderEvent responseReceivedFromProviderEvent = ResponseReceivedFromProviderEvent.Create(this.AggregateId, providerRequestReference, providerEmailReference); + + this.ApplyAndPend(responseReceivedFromProviderEvent); + } + + /// + /// Sends the request to provider. + /// + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + public void SendRequestToProvider(String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml) + { + RequestSentToProviderEvent requestSentToProviderEvent = RequestSentToProviderEvent.Create(this.AggregateId, fromAddress, toAddresses, subject, body, isHtml); + + this.ApplyAndPend(requestSentToProviderEvent); + } + + public String ProviderRequestReference { get; private set; } + public String ProviderEmailReference { get; private set; } + public String FromAddress { get; private set; } + public String Subject { get; private set; } + public String Body { get; private set; } + public Boolean IsHtml { get; private set; } + + private List Recipients; + + + /// + /// Gets the metadata. + /// + /// + [ExcludeFromCodeCoverage] + protected override Object GetMetadata() + { + return null; + } + + /// + /// Plays the event. + /// + /// The domain event. + protected override void PlayEvent(DomainEvent domainEvent) + { + this.PlayEvent((dynamic)domainEvent); + } + + private void PlayEvent(RequestSentToProviderEvent domainEvent) + { + this.Body = domainEvent.Body; + this.Subject = domainEvent.Subject; + this.IsHtml = domainEvent.IsHtml; + this.FromAddress = domainEvent.FromAddress; + + foreach (String domainEventToAddress in domainEvent.ToAddresses) + { + MessageRecipient messageRecipient = new MessageRecipient(); + messageRecipient.Create(domainEventToAddress); + this.Recipients.Add(messageRecipient); + } + } + + private void PlayEvent(ResponseReceivedFromProviderEvent domainEvent) + { + this.ProviderEmailReference = domainEvent.ProviderEmailReference; + this.ProviderRequestReference = domainEvent.ProviderRequestReference; + } + + #endregion + } + + internal class MessageRecipient + { + internal String ToAddress { get; private set; } + + internal MessageRecipient() + { + + } + + internal void Create(String toAddress) + { + this.ToAddress = toAddress; + } + } +} \ No newline at end of file diff --git a/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj b/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj new file mode 100644 index 0000000..318d813 --- /dev/null +++ b/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git a/MessagingService.sln b/MessagingService.sln new file mode 100644 index 0000000..0d0e167 --- /dev/null +++ b/MessagingService.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BF2482A1-13C0-4305-B732-AB62EBD9429B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9AEE6ADE-DD45-4605-A933-E06CF0BA4203}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagingService", "MessagingService\MessagingService.csproj", "{3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagingService.DataTransferObjects", "MessagingService.DataTransferObjects\MessagingService.DataTransferObjects.csproj", "{2E8C8118-8456-4CBD-BD68-578B838200CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessagingService.BusinessLogic", "MessagingService.BusinessLogic\MessagingService.BusinessLogic.csproj", "{A372C9E9-C412-4A36-961F-A358FF8F700E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingService.EmailMessageAggregate", "MessagingService.EmailMessageAggregate\MessagingService.EmailMessageAggregate.csproj", "{0BD162FA-64F1-44D5-9C06-749400F84FA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingService.EmailMessage.DomainEvents", "NessagingService.EmailMessage.DomainEvents\MessagingService.EmailMessage.DomainEvents.csproj", "{689A531D-86EB-4656-81B3-4C6E569A7E4B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F}.Release|Any CPU.Build.0 = Release|Any CPU + {2E8C8118-8456-4CBD-BD68-578B838200CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E8C8118-8456-4CBD-BD68-578B838200CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E8C8118-8456-4CBD-BD68-578B838200CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E8C8118-8456-4CBD-BD68-578B838200CC}.Release|Any CPU.Build.0 = Release|Any CPU + {A372C9E9-C412-4A36-961F-A358FF8F700E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A372C9E9-C412-4A36-961F-A358FF8F700E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A372C9E9-C412-4A36-961F-A358FF8F700E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A372C9E9-C412-4A36-961F-A358FF8F700E}.Release|Any CPU.Build.0 = Release|Any CPU + {0BD162FA-64F1-44D5-9C06-749400F84FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BD162FA-64F1-44D5-9C06-749400F84FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BD162FA-64F1-44D5-9C06-749400F84FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BD162FA-64F1-44D5-9C06-749400F84FA5}.Release|Any CPU.Build.0 = Release|Any CPU + {689A531D-86EB-4656-81B3-4C6E569A7E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {689A531D-86EB-4656-81B3-4C6E569A7E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {689A531D-86EB-4656-81B3-4C6E569A7E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {689A531D-86EB-4656-81B3-4C6E569A7E4B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3A9325F7-4A47-4B4E-8A99-58AEBD33FF3F} = {BF2482A1-13C0-4305-B732-AB62EBD9429B} + {2E8C8118-8456-4CBD-BD68-578B838200CC} = {BF2482A1-13C0-4305-B732-AB62EBD9429B} + {A372C9E9-C412-4A36-961F-A358FF8F700E} = {BF2482A1-13C0-4305-B732-AB62EBD9429B} + {0BD162FA-64F1-44D5-9C06-749400F84FA5} = {BF2482A1-13C0-4305-B732-AB62EBD9429B} + {689A531D-86EB-4656-81B3-4C6E569A7E4B} = {BF2482A1-13C0-4305-B732-AB62EBD9429B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1929C0FE-8CEB-4D0E-BD22-9E5E16E2B49F} + EndGlobalSection +EndGlobal diff --git a/MessagingService/Common/ClaimsHelper.cs b/MessagingService/Common/ClaimsHelper.cs new file mode 100644 index 0000000..a6e13e6 --- /dev/null +++ b/MessagingService/Common/ClaimsHelper.cs @@ -0,0 +1,114 @@ +using Shared.Exceptions; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace MessagingService.Common +{ + [ExcludeFromCodeCoverage] + public class ClaimsHelper + { + #region Methods + + /// + /// Gets the user claims. + /// + /// The user. + /// Type of the custom claim. + /// The default value. + /// + /// No claim [{customClaimType}] found for user id [{userIdClaim.Value} + public static Claim GetUserClaim(ClaimsPrincipal user, + String customClaimType, + String defaultValue = "") + { + Claim userClaim = null; + + if (ClaimsHelper.IsPasswordToken(user)) + { + // Get the claim from the token + userClaim = user.Claims.SingleOrDefault(c => c.Type == customClaimType); + + if (userClaim == null) + { + throw new NotFoundException($"Claim type [{customClaimType}] not found"); + } + } + else + { + userClaim = new Claim(customClaimType, defaultValue); + } + + return userClaim; + } + + /// + /// Determines whether [is client token] [the specified user]. + /// + /// The user. + /// + /// true if [is client token] [the specified user]; otherwise, false. + /// + public static Boolean IsPasswordToken(ClaimsPrincipal user) + { + Boolean result = false; + + Claim userIdClaim = user.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + + if (userIdClaim != null) + { + result = true; + } + + return result; + } + + /// + /// Validates the route parameter. + /// + /// + /// The route parameter. + /// The user claim. + public static Boolean ValidateRouteParameter(T routeParameter, + Claim userClaim) + { + if (routeParameter.ToString() != userClaim.Value) + { + return false; + } + + return true; + } + + /// + /// Determines whether [is user roles valid] [the specified user]. + /// + /// The user. + /// The allowed roles. + /// + /// true if [is user roles valid] [the specified user]; otherwise, false. + /// + public static Boolean IsUserRolesValid(ClaimsPrincipal user, String[] allowedRoles) + { + if (IsPasswordToken(user) == false) + { + return true; + } + + foreach (String allowedRole in allowedRoles) + { + if (user.IsInRole(allowedRole) == false) + { + return false; + } + } + + return true; + } + + #endregion + } +} diff --git a/MessagingService/Common/ConfigureSwaggerOptions.cs b/MessagingService/Common/ConfigureSwaggerOptions.cs new file mode 100644 index 0000000..a4c4479 --- /dev/null +++ b/MessagingService/Common/ConfigureSwaggerOptions.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MessagingService.Common +{ + using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Abstractions; + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Microsoft.OpenApi.Models; + using Swashbuckle.AspNetCore.SwaggerGen; + + /// + /// Configures the Swagger generation options. + /// + /// This allows API versioning to define a Swagger document per API version after the + /// service has been resolved from the service container. + [ExcludeFromCodeCoverage] + public class ConfigureSwaggerOptions : IConfigureOptions + { + #region Fields + + /// + /// The provider + /// + private readonly IApiVersionDescriptionProvider provider; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The provider used to generate Swagger documents. + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; + + #endregion + + #region Methods + + /// + public void Configure(SwaggerGenOptions options) + { + // add a swagger document for each discovered API version + // note: you might choose to skip or document deprecated API versions differently + foreach (ApiVersionDescription description in this.provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, ConfigureSwaggerOptions.CreateInfoForApiVersion(description)); + } + } + + /// + /// Creates the information for API version. + /// + /// The description. + /// + private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + OpenApiInfo info = new OpenApiInfo + { + Title = "Golf Handicapping API", + Version = description.ApiVersion.ToString(), + Description = "A REST Api to manage the golf club handicapping system.", + Contact = new OpenApiContact + { + Name = "Stuart Ferguson", + Email = "golfhandicapping@btinternet.com" + }, + License = new OpenApiLicense + { + Name = "TODO" + } + }; + + if (description.IsDeprecated) + { + info.Description += " This API version has been deprecated."; + } + + return info; + } + + #endregion + } + + /// + /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. + /// + /// This is only required due to bugs in the . + /// Once they are fixed and published, this class can be removed. + [ExcludeFromCodeCoverage] + public class SwaggerDefaultValues : IOperationFilter + { + /// + /// Applies the filter to the specified operation using the given context. + /// + /// The operation to apply the filter to. + /// The current operation filter context. + public void Apply(OpenApiOperation operation, + OperationFilterContext context) + { + ApiDescription apiDescription = context.ApiDescription; + ApiVersion apiVersion = apiDescription.GetApiVersion(); + ApiVersionModel model = apiDescription.ActionDescriptor.GetApiVersionModel(ApiVersionMapping.Explicit | ApiVersionMapping.Implicit); + + operation.Deprecated = model.DeprecatedApiVersions.Contains(apiVersion); + + if (operation.Parameters == null) + { + return; + } + + foreach (OpenApiParameter parameter in operation.Parameters) + { + ApiParameterDescription description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + if (parameter.Description == null) + { + parameter.Description = description.ModelMetadata?.Description; + } + + parameter.Required |= description.IsRequired; + } + } + } +} diff --git a/MessagingService/Common/SwaggerJsonConverter.cs b/MessagingService/Common/SwaggerJsonConverter.cs new file mode 100644 index 0000000..03c4fd8 --- /dev/null +++ b/MessagingService/Common/SwaggerJsonConverter.cs @@ -0,0 +1,80 @@ +namespace MessagingService.Common +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + public class SwaggerJsonConverter : JsonConverter + { + #region Properties + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// + /// true if this can read JSON; otherwise, false. + /// + public override Boolean CanRead => false; + + #endregion + + #region Methods + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override Boolean CanConvert(Type objectType) + { + return true; + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + /// + /// + public override Object ReadJson(JsonReader reader, + Type objectType, + Object existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, + Object value, + JsonSerializer serializer) + { + // Disable sending the $type in the serialized json + serializer.TypeNameHandling = TypeNameHandling.None; + + JToken t = JToken.FromObject(value); + t.WriteTo(writer); + } + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService/Controllers/EmailController.cs b/MessagingService/Controllers/EmailController.cs new file mode 100644 index 0000000..c5fc1d5 --- /dev/null +++ b/MessagingService/Controllers/EmailController.cs @@ -0,0 +1,106 @@ +using SendEmailRequestDTO = MessagingService.DataTransferObjects.SendEmailRequest; +using SendEmailResponseDTO = MessagingService.DataTransferObjects.SendEmailResponse; + +namespace MessagingService.Controllers +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using BusinessLogic.Requests; + using Common; + using MediatR; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + [Route(EmailController.ControllerRoute)] + [ApiController] + [ApiVersion("1.0")] + [Authorize] + public class EmailController : ControllerBase + { + #region Fields + + /// + /// The mediator + /// + private readonly IMediator Mediator; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The mediator. + public EmailController(IMediator mediator) + { + this.Mediator = mediator; + } + + #endregion + + #region Methods + + /// + /// Posts the email. + /// + /// The send email request. + /// The cancellation token. + /// + [HttpPost] + [Route("")] + public async Task PostEmail([FromBody] SendEmailRequestDTO sendEmailRequest, + CancellationToken cancellationToken) + { + // Reject password tokens + if (ClaimsHelper.IsPasswordToken(this.User)) + { + return this.Forbid(); + } + + Guid messageId = Guid.NewGuid(); + + // Create the command + SendEmailRequest request = SendEmailRequest.Create(sendEmailRequest.ConnectionIdentifier, + messageId, + sendEmailRequest.FromAddress, + sendEmailRequest.ToAddresses, + sendEmailRequest.Subject, + sendEmailRequest.Body, + sendEmailRequest.IsHtml); + + // Route the command + await this.Mediator.Send(request, cancellationToken); + + // return the result + return this.Created($"{EmailController.ControllerRoute}/{messageId}", + new SendEmailResponseDTO + { + MessageId = messageId + }); + } + + #endregion + + #region Others + + /// + /// The controller name + /// + public const String ControllerName = "email"; + + /// + /// The controller route + /// + private const String ControllerRoute = "api/" + EmailController.ControllerName; + + #endregion + } +} \ No newline at end of file diff --git a/MessagingService/Dockerfile b/MessagingService/Dockerfile new file mode 100644 index 0000000..fe4b032 --- /dev/null +++ b/MessagingService/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY ["MessagingService/MessagingService.csproj", "MessagingService/"] +RUN dotnet restore "MessagingService/MessagingService.csproj" +COPY . . +WORKDIR "/src/MessagingService" +RUN dotnet build "MessagingService.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "MessagingService.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "MessagingService.dll"] \ No newline at end of file diff --git a/MessagingService/MessagingService.csproj b/MessagingService/MessagingService.csproj new file mode 100644 index 0000000..9fb5660 --- /dev/null +++ b/MessagingService/MessagingService.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.1 + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MessagingService/Program.cs b/MessagingService/Program.cs new file mode 100644 index 0000000..8b9c011 --- /dev/null +++ b/MessagingService/Program.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MessagingService +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class Program + { + public static void Main(string[] args) + { + Program.CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + Console.Title = "Messaging Service"; + + //At this stage, we only need our hosting file for ip and ports + IConfigurationRoot config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile("hosting.development.json", optional: true) + .AddEnvironmentVariables().Build(); + + IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); + hostBuilder.ConfigureLogging(logging => + { + logging.AddConsole(); + + }); + hostBuilder.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseConfiguration(config); + webBuilder.UseKestrel(); + }); + return hostBuilder; + } + + } +} diff --git a/MessagingService/Properties/launchSettings.json b/MessagingService/Properties/launchSettings.json new file mode 100644 index 0000000..4eedb6b --- /dev/null +++ b/MessagingService/Properties/launchSettings.json @@ -0,0 +1,36 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50775", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "MessagingService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", + "publishAllPorts": true + } + } +} \ No newline at end of file diff --git a/MessagingService/Startup.cs b/MessagingService/Startup.cs new file mode 100644 index 0000000..013af96 --- /dev/null +++ b/MessagingService/Startup.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MessagingService +{ + using System.IO; + using System.Net.Http; + using System.Reflection; + using BusinessLogic.Common; + using BusinessLogic.RequestHandlers; + using BusinessLogic.Requests; + using BusinessLogic.Services; + using BusinessLogic.Services.EmailServices; + using BusinessLogic.Services.EmailServices.Smtp2Go; + using Common; + using EmailMessageAggregate; + using EventStore.ClientAPI; + using MediatR; + using Microsoft.AspNetCore.Authentication.JwtBearer; + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.Options; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + using NLog.Extensions.Logging; + using Shared.DomainDrivenDesign.EventStore; + using Shared.EntityFramework.ConnectionStringConfiguration; + using Shared.EventStore.EventStore; + using Shared.Extensions; + using Shared.General; + using Shared.Logger; + using Shared.Repositories; + using Swashbuckle.AspNetCore.Filters; + using Swashbuckle.AspNetCore.SwaggerGen; + using ILogger = EventStore.ClientAPI.ILogger; + + public class Startup + { + public Startup(IWebHostEnvironment webHostEnvironment) + { + IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(webHostEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{webHostEnvironment.EnvironmentName}.json", optional: true).AddEnvironmentVariables(); + + Startup.Configuration = builder.Build(); + Startup.WebHostEnvironment = webHostEnvironment; + } + + public static IConfigurationRoot Configuration { get; set; } + + public static IWebHostEnvironment WebHostEnvironment { get; set; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + this.ConfigureMiddlewareServices(services); + + services.AddTransient(); + + ConfigurationReader.Initialise(Startup.Configuration); + String connString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + String connectionName = Startup.Configuration.GetValue("EventStoreSettings:ConnectionName"); + Int32 httpPort = Startup.Configuration.GetValue("EventStoreSettings:HttpPort"); + + Boolean useConnectionStringConfig = Boolean.Parse(ConfigurationReader.GetValue("AppSettings", "UseConnectionStringConfig")); + EventStoreConnectionSettings settings = EventStoreConnectionSettings.Create(connString, connectionName, httpPort); + services.AddSingleton(settings); + + services.AddSingleton>(cont => (connectionSettings) => + { + return EventStoreConnection.Create(connectionSettings + .ConnectionString); + }); + + services.AddSingleton>(cont => (connectionString) => + { + EventStoreConnectionSettings connectionSettings = + EventStoreConnectionSettings.Create(connectionString, connectionName, httpPort); + + Func eventStoreConnectionFunc = cont.GetService>(); + + IEventStoreContext context = + new EventStoreContext(connectionSettings, eventStoreConnectionFunc); + + return context; + }); + + + if (useConnectionStringConfig) + { + String connectionStringConfigurationConnString = ConfigurationReader.GetConnectionString("ConnectionStringConfiguration"); + services.AddSingleton(); + services.AddTransient(c => + { + return new ConnectionStringConfigurationContext(connectionStringConfigurationConnString); + }); + + services.AddSingleton(c => + { + Func contextFunc = c.GetService>(); + IConnectionStringConfigurationRepository connectionStringConfigurationRepository = + c.GetService(); + return new EventStoreContextManager(contextFunc, + connectionStringConfigurationRepository); + }); + } + else + { + services.AddSingleton(c => + { + IEventStoreContext context = c.GetService(); + return new EventStoreContextManager(context); + }); + services.AddSingleton(); + } + + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton, AggregateRepository>(); + services.AddSingleton(); + services.AddSingleton(); + //services.AddSingleton(); + //services.AddSingleton(); + //services.AddSingleton(); + + //// request & notification handlers + services.AddTransient(context => + { + return t => context.GetService(t); + }); + + services.AddSingleton, EmailRequestHandler>(); + + services.AddSingleton>(container => (serviceName) => + { + return ConfigurationReader.GetBaseServerUri(serviceName).OriginalString; + }); + services.AddSingleton(); + } + + private void ConfigureMiddlewareServices(IServiceCollection services) + { + services.AddApiVersioning( + options => + { + // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions" + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = new HeaderApiVersionReader("api-version"); + }); + + services.AddVersionedApiExplorer( + options => + { + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; + + // note: this option is only necessary when versioning by url segment. the SubstitutionFormat + // can also be used to control the format of the API version in route templates + options.SubstituteApiVersionInUrl = true; + }); + + services.AddTransient, ConfigureSwaggerOptions>(); + + services.AddSwaggerGen(c => + { + // add a custom operation filter which sets default values + c.OperationFilter(); + c.ExampleFilters(); + }); + + services.AddSwaggerExamplesFromAssemblyOf(); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + //options.SaveToken = true; + options.Authority = ConfigurationReader.GetValue("SecurityConfiguration", "Authority"); + options.Audience = ConfigurationReader.GetValue("SecurityConfiguration", "ApiName"); + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = ConfigurationReader.GetValue("SecurityConfiguration", "ApiName"), + ValidIssuer = ConfigurationReader.GetValue("SecurityConfiguration", "Authority"), + }; + options.IncludeErrorDetails = true; + }); + + services.AddControllers().AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + Assembly assembly = this.GetType().GetTypeInfo().Assembly; + services.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, + IApiVersionDescriptionProvider provider) + { + String nlogConfigFilename = "nlog.config"; + + if (env.IsDevelopment()) + { + nlogConfigFilename = $"nlog.{env.EnvironmentName}.config"; + app.UseDeveloperExceptionPage(); + } + + loggerFactory.ConfigureNLog(Path.Combine(env.ContentRootPath, nlogConfigFilename)); + loggerFactory.AddNLog(); + + Microsoft.Extensions.Logging.ILogger logger = loggerFactory.CreateLogger("EstateManagement"); + + Logger.Initialise(logger); + + ConfigurationReader.Initialise(Startup.Configuration); + + app.AddRequestLogging(); + app.AddResponseLogging(); + app.AddExceptionHandler(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + app.UseSwagger(); + + app.UseSwaggerUI( + options => + { + // build a swagger endpoint for each discovered API version + foreach (ApiVersionDescription description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + } + } +} diff --git a/MessagingService/appsettings.Development.json b/MessagingService/appsettings.Development.json new file mode 100644 index 0000000..7ea7725 --- /dev/null +++ b/MessagingService/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "EventStoreSettings": { + "ConnectionString": "ConnectTo=tcp://admin:changeit@127.0.0.1:1113;VerboseLogging=true;", + "ConnectionName": "Messaging Service", + "HttpPort": 2113, + "START_PROJECTIONS": false, + "ContinuousProjectionsFolder": "" + }, + "ConnectionStrings": { + "ConnectionStringConfiguration": "server=127.0.0.1;database=ConnectionStringConfiguration;user id=sa;password=Sc0tland", + } +} diff --git a/MessagingService/appsettings.json b/MessagingService/appsettings.json new file mode 100644 index 0000000..0935e8a --- /dev/null +++ b/MessagingService/appsettings.json @@ -0,0 +1,31 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "EventStoreSettings": { + "ConnectionString": "ConnectTo=tcp://admin:changeit@192.168.1.133:1113;VerboseLogging=true;", + "ConnectionName": "Estate Management", + "HttpPort": 2113, + "START_PROJECTIONS": false, + "ContinuousProjectionsFolder": "" + }, + "ConnectionStrings": { + "ConnectionStringConfiguration": "server=192.168.1.133;database=ConnectionStringConfiguration;user id=sa;password=Sc0tland", + "EstateReportingReadModel": "server=localhost;user id=sa;password=sp1ttal;database=EstateReportingReadModel" + }, + "AppSettings": { + "UseConnectionStringConfig": false, + "SecurityService": "http://192.168.1.133:5001", + "SMTP2GoBaseAddress": "https://api.smtp2go.com/v3/", + "SMTP2GoAPIKey": "api-4CE2C6BC80D111EAB45BF23C91C88F4E" + }, + "SecurityConfiguration": { + "ApiName": "messagingService", + "Authority": "http://192.168.1.133:5001" + }, + "AllowedHosts": "*" +} diff --git a/MessagingService/hosting.json b/MessagingService/hosting.json new file mode 100644 index 0000000..759a312 --- /dev/null +++ b/MessagingService/hosting.json @@ -0,0 +1,3 @@ +{ + "urls": "http://*:5006" +} \ No newline at end of file diff --git a/MessagingService/nlog.config b/MessagingService/nlog.config new file mode 100644 index 0000000..da18452 --- /dev/null +++ b/MessagingService/nlog.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MessagingService/nlog.development.config b/MessagingService/nlog.development.config new file mode 100644 index 0000000..99744c8 --- /dev/null +++ b/MessagingService/nlog.development.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/NessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj b/NessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj new file mode 100644 index 0000000..44c1c40 --- /dev/null +++ b/NessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.1 + + + + + + + diff --git a/NessagingService.EmailMessage.DomainEvents/RequestSentToProviderEvent.cs b/NessagingService.EmailMessage.DomainEvents/RequestSentToProviderEvent.cs new file mode 100644 index 0000000..8b40d5d --- /dev/null +++ b/NessagingService.EmailMessage.DomainEvents/RequestSentToProviderEvent.cs @@ -0,0 +1,126 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class RequestSentToProviderEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public RequestSentToProviderEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + private RequestSentToProviderEvent(Guid aggregateId, + Guid eventId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml) : base(aggregateId, eventId) + { + this.FromAddress = fromAddress; + this.ToAddresses = toAddresses; + this.Subject = subject; + this.Body = body; + this.IsHtml = isHtml; + } + + #endregion + + #region Properties + + /// + /// Gets the body. + /// + /// + /// The body. + /// + [JsonProperty] + public String Body { get; private set; } + + /// + /// Gets from address. + /// + /// + /// From address. + /// + [JsonProperty] + public String FromAddress { get; private set; } + + /// + /// Gets a value indicating whether this instance is HTML. + /// + /// + /// true if this instance is HTML; otherwise, false. + /// + [JsonProperty] + public Boolean IsHtml { get; private set; } + + /// + /// Gets the subject. + /// + /// + /// The subject. + /// + [JsonProperty] + public String Subject { get; private set; } + + /// + /// Converts to addresses. + /// + /// + /// To addresses. + /// + [JsonProperty] + public List ToAddresses { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// From address. + /// To addresses. + /// The subject. + /// The body. + /// if set to true [is HTML]. + /// + public static RequestSentToProviderEvent Create(Guid aggregateId, + String fromAddress, + List toAddresses, + String subject, + String body, + Boolean isHtml) + { + return new RequestSentToProviderEvent(aggregateId, Guid.NewGuid(), fromAddress, toAddresses, subject, body, isHtml); + } + + #endregion + } +} \ No newline at end of file diff --git a/NessagingService.EmailMessage.DomainEvents/ResponseReceivedFromProvider.cs b/NessagingService.EmailMessage.DomainEvents/ResponseReceivedFromProvider.cs new file mode 100644 index 0000000..1e96555 --- /dev/null +++ b/NessagingService.EmailMessage.DomainEvents/ResponseReceivedFromProvider.cs @@ -0,0 +1,83 @@ +namespace MessagingService.EmailMessage.DomainEvents +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + /// + /// + /// + /// + public class ResponseReceivedFromProviderEvent : DomainEvent + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public ResponseReceivedFromProviderEvent() + { + //We need this for serialisation, so just embrace the DDD crime + } + + /// + /// Initializes a new instance of the class. + /// + /// The aggregate identifier. + /// The event identifier. + /// The provider request reference. + /// The provider email reference. + private ResponseReceivedFromProviderEvent(Guid aggregateId, + Guid eventId, + String providerRequestReference, + String providerEmailReference) : base(aggregateId, eventId) + { + this.ProviderRequestReference = providerRequestReference; + this.ProviderEmailReference = providerEmailReference; + } + + #endregion + + #region Properties + + /// + /// Gets the provider email reference. + /// + /// + /// The provider email reference. + /// + [JsonProperty] + public String ProviderEmailReference { get; private set; } + + /// + /// Gets the provider request reference. + /// + /// + /// The provider request reference. + /// + [JsonProperty] + public String ProviderRequestReference { get; private set; } + + #endregion + + #region Methods + + /// + /// Creates the specified aggregate identifier. + /// + /// The aggregate identifier. + /// The provider request reference. + /// The provider email reference. + /// + public static ResponseReceivedFromProviderEvent Create(Guid aggregateId, + String providerRequestReference, + String providerEmailReference) + { + return new ResponseReceivedFromProviderEvent(aggregateId, Guid.NewGuid(), providerRequestReference, providerEmailReference); + } + + #endregion + } +} \ No newline at end of file