Skip to content

Commit

Permalink
Replace Background MQ with Background Jobs
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Sep 12, 2024
1 parent 2f964c0 commit e39ae1a
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 105 deletions.
30 changes: 19 additions & 11 deletions MyApp.ServiceInterface/EmailServices.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Net.Mail;
using Microsoft.Extensions.Logging;
using ServiceStack;
using MyApp.ServiceModel;
using ServiceStack.Jobs;

namespace MyApp.ServiceInterface;

Expand Down Expand Up @@ -45,16 +45,26 @@ public class SmtpConfig
public string? Bcc { get; set; }
}

/// <summary>
/// Uses a configured SMTP client to send emails
/// </summary>
public class EmailServices(SmtpConfig config, ILogger<EmailServices> log)
// TODO: Uncomment to enable sending emails with SMTP
// : Service
public class SendEmail
{
public string To { get; set; }

Check warning on line 50 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'To' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 50 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / push_to_registry

Non-nullable property 'To' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 50 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / push_to_registry

Non-nullable property 'To' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string? ToName { get; set; }
public string Subject { get; set; }

Check warning on line 52 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Subject' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 52 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / push_to_registry

Non-nullable property 'Subject' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 52 in MyApp.ServiceInterface/EmailServices.cs

View workflow job for this annotation

GitHub Actions / push_to_registry

Non-nullable property 'Subject' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string? BodyText { get; set; }
public string? BodyHtml { get; set; }
}

[Worker("smtp")]
public class SendEmailCommand(ILogger<SendEmailCommand> logger, IBackgroundJobs jobs, SmtpConfig config)
: SyncCommand<SendEmail>
{
public object Any(SendEmail request)
private static long count = 0;
protected override void Run(SendEmail request)
{
log.LogInformation("Sending email to {Email} with subject {Subject}", request.To, request.Subject);
Interlocked.Increment(ref count);
var log = Request.CreateJobLogger(jobs, logger);
log.LogInformation("Sending {Count} email to {Email} with subject {Subject}",
count, request.To, request.Subject);

using var client = new SmtpClient(config.Host, config.Port);
client.Credentials = new System.Net.NetworkCredential(config.Username, config.Password);
Expand All @@ -80,7 +90,5 @@ public object Any(SendEmail request)
}

client.Send(msg);

return new EmptyResponse();
}
}
15 changes: 0 additions & 15 deletions MyApp.ServiceModel/Emails.cs

This file was deleted.

20 changes: 20 additions & 0 deletions MyApp/Areas/Identity/Pages/Account/IdentityNoOpEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using MyApp.Data;

namespace MyApp.Areas.Identity.Pages.Account;

// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly IEmailSender emailSender = new NoOpEmailSender();

public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");

public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");

public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
90 changes: 90 additions & 0 deletions MyApp/Configure.BackgroundJobs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Identity;
using ServiceStack.Jobs;
using MyApp.Data;
using MyApp.ServiceInterface;

[assembly: HostingStartup(typeof(MyApp.ConfigureBackgroundJobs))]

namespace MyApp;

public class ConfigureBackgroundJobs : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices((context,services) => {
var smtpConfig = context.Configuration.GetSection(nameof(SmtpConfig))?.Get<SmtpConfig>();
if (smtpConfig is not null)
{
services.AddSingleton(smtpConfig);
}
// Lazily register SendEmailCommand to allow SmtpConfig to only be required if used
services.AddTransient<SendEmailCommand>(c => new SendEmailCommand(
c.GetRequiredService<ILogger<SendEmailCommand>>(),
c.GetRequiredService<IBackgroundJobs>(),
c.GetRequiredService<SmtpConfig>()));
services.AddPlugin(new CommandsFeature());
services.AddPlugin(new BackgroundsJobFeature());
services.AddHostedService<JobsHostedService>();
}).ConfigureAppHost(afterAppHostInit: appHost => {
var services = appHost.GetApplicationServices();
// Log if EmailSender is enabled and SmtpConfig missing
var log = services.GetRequiredService<ILogger<ConfigureBackgroundJobs>>();
var emailSender = services.GetRequiredService<IEmailSender<ApplicationUser>>();
if (emailSender is EmailSender)
{
var smtpConfig = services.GetService<SmtpConfig>();
if (smtpConfig is null)
{
log.LogWarning("SMTP is not configured, please configure SMTP to enable sending emails");
}
else
{
log.LogWarning("SMTP is configured with <{FromEmail}> {FromName}", smtpConfig.FromEmail, smtpConfig.FromName);
}
}
var jobs = services.GetRequiredService<IBackgroundJobs>();
// Example of registering a Recurring Job to run Every Hour
//jobs.RecurringCommand<MyCommand>(Schedule.Hourly);
});
}

public class JobsHostedService(ILogger<JobsHostedService> log, IBackgroundJobs jobs) : BackgroundService

Check warning on line 53 in MyApp/Configure.BackgroundJobs.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'log' is unread.
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await jobs.StartAsync(stoppingToken);

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
await jobs.TickAsync();
}
}
}

/// <summary>
/// Sends emails by executing SendEmailCommand in a background job where it's serially processed by 'smtp' worker
/// </summary>
public class EmailSender(IBackgroundJobs jobs) : IEmailSender<ApplicationUser>
{
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
jobs.EnqueueCommand<SendEmailCommand>(new SendEmail {
To = email,
Subject = subject,
BodyHtml = htmlMessage,
});
return Task.CompletedTask;
}

public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");

public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");

public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
79 changes: 0 additions & 79 deletions MyApp/Configure.Mq.cs

This file was deleted.

1 change: 1 addition & 0 deletions MyApp/MyApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageReference Include="ServiceStack" Version="8.*" />
<PackageReference Include="ServiceStack.Mvc" Version="8.*" />
<PackageReference Include="ServiceStack.Server" Version="8.*" />
<PackageReference Include="ServiceStack.Jobs" Version="8.*" />
<PackageReference Include="ServiceStack.Extensions" Version="8.*" />
<PackageReference Include="ServiceStack.OrmLite.Sqlite.Data" Version="8.*" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions MyApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using MyApp.Areas.Identity.Pages.Account;
using MyApp.ServiceInterface;

var builder = WebApplication.CreateBuilder(args);
Expand Down

0 comments on commit e39ae1a

Please sign in to comment.