Skip to content

Commit

Permalink
Notification audit package - log sent notifications (#14)
Browse files Browse the repository at this point in the history
Co-authored-by: Khaperskaia, Anna <[email protected]>
Co-authored-by: Artem Leshchev <[email protected]>
  • Loading branch information
3 people authored May 29, 2024
1 parent a47c471 commit 806477a
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 40 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This repository offers a wide collection of .NET packages for use in microservic
- [Kubernetes Health Checks](#Health-Checks)
- [NHibernate](#NHibernate)
- [Notifications](#Notifications)
- [Notifications Audit](#Notifications-Audit)

## RabbitMQ

Expand Down Expand Up @@ -319,4 +320,21 @@ public class YourNotificationRequestHandler(IEmailSender sender) : IRequestHandl
```

> [!NOTE]
> Note that attachment will be inlined only if its 'Inline' field is true and its name is referred as image source in message body.
> Attachment will be added to notification only if:
> - it is not inlined
> - it is inlined and referred by name as image source in notification text
### Notifications Audit

To audit sent notifications, first install the [NuGet package](https://www.nuget.org/packages/Luxoft.Bss.Platform.Notifications.Audit):
```shell
dotnet add package Luxoft.Bss.Platform.Notifications.Audit
```

Then register notifications service in DI with provided sql connection
```C#
services
.AddPlatformNotificationsAudit(o => o.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection")!);
```

Thats all - db schema and tables will be generated on application start (you can customize schema and table names on DI step).
9 changes: 7 additions & 2 deletions src/Bss.Platform.Api.Documentation/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.SwaggerGen;

namespace Bss.Platform.Api.Documentation;

public static class DependencyInjection
Expand All @@ -15,7 +17,8 @@ public static class DependencyInjection
public static IServiceCollection AddPlatformApiDocumentation(
this IServiceCollection services,
IWebHostEnvironment hostEnvironment,
string title = "API")
string title = "API",
Action<SwaggerGenOptions>? setupAction = null)
{
if (hostEnvironment.IsProduction())
{
Expand All @@ -27,8 +30,8 @@ public static IServiceCollection AddPlatformApiDocumentation(
.AddSwaggerGen(
x =>
{
x.SchemaFilter<XEnumNamesSchemaFilter>();
x.SwaggerDoc("api", new OpenApiInfo { Title = title });
x.SchemaFilter<XEnumNamesSchemaFilter>();
x.AddSecurityDefinition(
AuthorizationScheme,
Expand All @@ -52,6 +55,8 @@ public static IServiceCollection AddPlatformApiDocumentation(
new List<string>()
}
});
setupAction?.Invoke(x);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Luxoft.Bss.Platform.Notifications.Audit</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Bss.Platform.Notifications\Bss.Platform.Notifications.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper"/>
<PackageReference Include="Microsoft.SqlServer.SqlManagementObjects"/>
</ItemGroup>
</Project>
24 changes: 24 additions & 0 deletions src/Bss.Platform.Notifications.Audit/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Bss.Platform.Notifications.Audit.Models;
using Bss.Platform.Notifications.Audit.Services;
using Bss.Platform.Notifications.Interfaces;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Bss.Platform.Notifications.Audit;

public static class DependencyInjection
{
public static IServiceCollection AddPlatformNotificationsAudit(
this IServiceCollection services,
Action<NotificationAuditOptions>? setup = null)
{
var settings = new NotificationAuditOptions();
setup?.Invoke(settings);

return services
.AddHostedService<AuditSchemaMigrationService>()
.AddScoped<IAuditService, AuditService>()
.AddSingleton<IOptions<NotificationAuditOptions>>(_ => Options.Create(settings));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Bss.Platform.Notifications.Audit.Models;

public class NotificationAuditOptions
{
public string Schema { get; set; } = "notifications";

public string Table { get; set; } = "SentMessages";

public string ConnectionString { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Bss.Platform.Notifications.Audit.Models;

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;

namespace Bss.Platform.Notifications.Audit.Services;

public class AuditSchemaMigrationService(ILogger<AuditSchemaMigrationService> logger, IOptions<NotificationAuditOptions> settings)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await using var connection = new SqlConnection(settings.Value.ConnectionString);
await connection.OpenAsync(stoppingToken);

var server = new Server(new ServerConnection(connection));
var catalog = server.ConnectionContext.CurrentDatabase;

if (string.IsNullOrWhiteSpace(catalog) || !server.Databases.Contains(catalog))
{
throw new ArgumentException("Initial catalog not provided or does not exist");
}

var database = server.Databases[catalog];

if (!database.Tables.Contains(settings.Value.Table, settings.Value.Schema))
{
if (!database.Schemas.Contains(settings.Value.Schema))
{
logger.LogInformation("Creating schema [{Schema}] ...", settings.Value.Schema);
server.ConnectionContext.ExecuteNonQuery($"CREATE SCHEMA [{settings.Value.Schema}]");
}

logger.LogInformation("Creating table [{Table}] ...", settings.Value.Table);
server.ConnectionContext.ExecuteNonQuery(
$"""
CREATE TABLE [{settings.Value.Schema}].[{settings.Value.Table}] (
[id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[from] [nvarchar](255) NOT NULL,
[to] [nvarchar](max) NULL,
[copy] [nvarchar](max) NULL,
[replyTo] [nvarchar](max) NULL,
[subject] [nvarchar](max) NULL,
[message] [nvarchar](max) NULL,
[timestamp] [datetime2](7) NOT NULL)
""");
}

await connection.CloseAsync();

logger.LogInformation("Schema migration successfully complete");
}
catch (Exception e)
{
logger.LogError(e, "Schema migration was failed");
throw;
}
}
}
47 changes: 47 additions & 0 deletions src/Bss.Platform.Notifications.Audit/Services/AuditService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Net.Mail;

using Bss.Platform.Notifications.Audit.Models;
using Bss.Platform.Notifications.Interfaces;

using Dapper;

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Bss.Platform.Notifications.Audit.Services;

public class AuditService(ILogger<AuditService> logger, IOptions<NotificationAuditOptions> settings) : IAuditService
{
private const string Sql = """
insert into [{0}].[{1}]
([id], [from], [to], [copy], [replyTo], [subject], [message], [timestamp])
values
(newid(), @from, @to, @copy, @replyTo, @subject, @message, getdate())
""";

public async Task LogAsync(MailMessage message, CancellationToken token)
{
try
{
await using var db = new SqlConnection(settings.Value.ConnectionString);
await db.OpenAsync(token);

await db.ExecuteAsync(
string.Format(Sql, settings.Value.Schema, settings.Value.Table),
new
{
from = message.From!.Address,
to = string.Join(";", message.To.Select(x => x.Address)),
copy = string.Join(";", message.CC.Select(x => x.Address)),
replyTo = string.Join(";", message.ReplyToList.Select(x => x.Address)),
subject = message.Subject,
message = message.Body
});
}
catch (Exception e)
{
logger.LogError(e, "Failed to log sent message");
}
}
}
13 changes: 6 additions & 7 deletions src/Bss.Platform.Notifications/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,27 @@ public static IServiceCollection AddPlatformNotifications(
{
var settings = configuration.GetSection(NotificationSenderOptions.SectionName).Get<NotificationSenderOptions>()!;

return AddTestEnvironmentRedirection(services, hostEnvironment, settings)
.AddMailMessageSenders(settings)
.Configure<NotificationSenderOptions>(configuration.GetSection(NotificationSenderOptions.SectionName))
.AddScoped<IEmailSender, EmailSender>();
return services.AddEmailSender(hostEnvironment, settings)
.AddMailMessageSenders(settings)
.Configure<NotificationSenderOptions>(configuration.GetSection(NotificationSenderOptions.SectionName));
}

private static IServiceCollection AddTestEnvironmentRedirection(
private static IServiceCollection AddEmailSender(
this IServiceCollection services,
IHostEnvironment hostEnvironment,
NotificationSenderOptions settings)
{
if (hostEnvironment.IsProduction())
{
return services;
return services.AddScoped<IEmailSender, EmailSender>();
}

if (settings.RedirectTo?.Length == 0)
{
throw new ArgumentException("Test email address is not provided");
}

return services.AddScoped<IRedirectService, RedirectService>();
return services.AddScoped<IEmailSender, EmailSenderTest>();
}

private static IServiceCollection AddMailMessageSenders(this IServiceCollection services, NotificationSenderOptions settings)
Expand Down
8 changes: 8 additions & 0 deletions src/Bss.Platform.Notifications/Interfaces/IAuditService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Net.Mail;

namespace Bss.Platform.Notifications.Interfaces;

public interface IAuditService
{
Task LogAsync(MailMessage message, CancellationToken token);
}
8 changes: 0 additions & 8 deletions src/Bss.Platform.Notifications/Interfaces/IRedirectService.cs

This file was deleted.

18 changes: 11 additions & 7 deletions src/Bss.Platform.Notifications/Services/EmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@

namespace Bss.Platform.Notifications.Services;

internal class EmailSender(IEnumerable<IMailMessageSender> senders, IRedirectService? redirectService = null) : IEmailSender
internal class EmailSender(IEnumerable<IMailMessageSender> senders, IAuditService? auditService = null) : IEmailSender
{
public async Task<MailMessage> SendAsync(EmailModel model, CancellationToken token)
public async Task<MailMessage> SendAsync(EmailModel emailModel, CancellationToken token)
{
var message = Convert(model);

redirectService?.Redirect(message);
var message = this.Convert(emailModel);

foreach (var sender in senders)
{
await sender.SendAsync(message, token);
}

if (auditService is not null)
{
await auditService.LogAsync(message, token);
}

return message;
}

private static MailMessage Convert(EmailModel model)
protected virtual MailMessage Convert(EmailModel model)
{
var mailMessage = new MailMessage { Subject = model.Subject, Body = model.Body, From = model.From, IsBodyHtml = true };

Expand Down Expand Up @@ -50,9 +53,9 @@ private static void SetAttachments(Attachment[] attachments, MailMessage mailMes
{
foreach (var attachment in attachments)
{
mailMessage.Attachments.Add(attachment);
if (!attachment.ContentDisposition!.Inline)
{
mailMessage.Attachments.Add(attachment);
continue;
}

Expand All @@ -63,6 +66,7 @@ private static void SetAttachments(Attachment[] attachments, MailMessage mailMes
}

mailMessage.Body = Regex.Replace(mailMessage.Body, srcRegex, $"src=\"cid:{attachment.ContentId}\"", RegexOptions.IgnoreCase);
mailMessage.Attachments.Add(attachment);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@

namespace Bss.Platform.Notifications.Services;

internal class RedirectService(IOptions<NotificationSenderOptions> settings) : IRedirectService
internal class EmailSenderTest(
IEnumerable<IMailMessageSender> senders,
IOptions<NotificationSenderOptions> settings,
IAuditService? auditService = null)
: EmailSender(senders, auditService)
{
public void Redirect(MailMessage message)
protected override MailMessage Convert(EmailModel model)
{
var message = base.Convert(model);
this.ChangeRecipients(message);

return message;
}

private void ChangeRecipients(MailMessage message)
{
AddRecipientsToBody(message);

Expand All @@ -21,14 +33,6 @@ public void Redirect(MailMessage message)
}
}

private static void ClearRecipients(MailMessage message)
{
message.To.Clear();
message.CC.Clear();
message.Bcc.Clear();
message.ReplyToList.Clear();
}

private static void AddRecipientsToBody(MailMessage message)
{
var originalRecipients =
Expand All @@ -39,4 +43,12 @@ private static void AddRecipientsToBody(MailMessage message)

message.Body = $"{originalRecipients}{message.Body}";
}

private static void ClearRecipients(MailMessage message)
{
message.To.Clear();
message.CC.Clear();
message.Bcc.Clear();
message.ReplyToList.Clear();
}
}
Loading

0 comments on commit 806477a

Please sign in to comment.