Part 5: Implementing a Notification Service for Sending Emails with Event Driven Domain using .Net 8

Fábio Salomão
7 min readJun 10, 2024

--

In the previous part, we added invoice persistence to a database. Now, let’s create a notification service that will consume invoice events and send emails with the invoice details.

Creating the .Net 8 Project for the Notification Service

  1. Creating the Project: Open your solution “EventDriven” and create a new project of type ASP.NET Core Web API.
  2. Configuring the Project: Name the project NotificationService and select .Net 8 as the framework version.
  3. Adding Dependencies: Add the following dependencies to your project:
dotnet add package RabbitMQ.Client
dotnet add package Microsoft.Extensions.Hosting
dotnet add package SendGrid

Configuring Email Sending with SendGrid

To ensure consistent email delivery, we will use SendGrid, one of the most widely used email services in the world. SendGrid offers a free tier that can be accessed directly from their website or, if you have an Azure account, can be configured through the Azure Portal.

Step-by-step Guide to Create a SendGrid Account

Creating an Account via SendGrid Website

  1. Visit the SendGrid website: Go to sendgrid.com and click on “Sign Up” to create a new account.
  2. Fill out the registration form: Enter the necessary details such as name, email, and create a password. Click on “Create Account”.
  3. Confirm your email: Check your inbox for a confirmation email from SendGrid and follow the instructions to confirm your account.
  4. Set up your account: After confirmation, log in to SendGrid and follow the initial setup wizard to complete your profile and set up your email sending preferences.
  5. Set up and generate the API Key: After creating the instance, go to the “Settings” → “API Keys” section in the SendGrid dashboard and create a new API key to use in your applications.

Creating an Account via Azure Portal

  1. Access the Azure Portal: Go to portal.azure.com and log in with your Azure account.
  2. Navigate to the services section: In the left-hand menu, click on “Create a resource” and search for “SendGrid”.
  3. Select SendGrid and create a new instance: Choose “SendGrid” from the search results and click on “Create”.
  4. Fill out the instance details: Enter the necessary information such as the instance name, pricing plan (select the free tier to start), and configure other options as needed.
  5. Open de SendGrid Dashboard: After creating the instance, open the SendGrid dashboard by clicking on the “Open a SaaS Account on the publisher’s website” link.
  6. Set up and generate the API Key: In the SendGrid Dashboard, go to the “Settings” → “API Keys” section and create a new API key to use in your applications.

Configuration in appsettings.json

After creating your account and obtaining the API Key, add the configuration to your project’s appsettings.json file:

{
"SendGridSettings": {
"ApiKey": "API_KEY",
"FromEmail": "EMAIL_FROM",
"FromName": "SERVICE_NAME"
}
}

Creating the Email Service

Create a folder named Services and add the following interface:

namespace NotificationService.Services
{
public interface IEmailService
{
Task SendEmailAsync(string toEmail, string subject, string body);
}
}

In the same folder, create the EmailService class implementing the IEmailService interface:

using SendGrid;
using SendGrid.Helpers.Mail;

namespace NotificationService.Services
{
public class EmailService : IEmailService
{
private readonly IConfiguration _configuration;

public EmailService(IConfiguration configuration)
{
_configuration = configuration;
}

public async Task SendEmailAsync(string toEmail, string subject, string body)
{
var sendgridApiKey = _configuration["SendGridSettings:ApiKey"];
var senderEmail = _configuration["SendGridSettings:FromEmail"];
var senderName = _configuration["SendGridSettings:FromName"];

var client = new SendGridClient(sendgridApiKey);
var from = new EmailAddress(senderEmail, senderName);
var to = new EmailAddress(toEmail);
var msg = MailHelper.CreateSingleEmail(from, to, subject, body, body);
var response = await client.SendEmailAsync(msg);

if (response.StatusCode != System.Net.HttpStatusCode.Accepted)
{
// Handle error
var responseBody = await response.Body.ReadAsStringAsync();
throw new Exception($"Failed to send email: {response.StatusCode}, {responseBody}");
}
}
}
}

Implementing the Event Consumer

Invoice Model

Add an Invoice class similar to the one used in the billing service:

namespace NotificationService.Models
{
public class Invoice
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total => Quantity * Price;
public DateTime CreatedAt { get; set; }
}
}

Event Consumer (Subscriber)

Create a service to consume events. Add the following interface:

namespace NotificationService.Services
{
public interface IEventSubscriber
{
void Subscribe();
}
}

In the same folder, create the RabbitMqEventSubscriber class implementing the IEventSubscriber interface. Replace the recipient email address with your preferred one:

using NotificationService.Models;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Text.Json;

namespace NotificationService.Services
{
public class RabbitMqEventSubscriber : IEventSubscriber
{
private readonly IConnection _connection;
private readonly IEmailService _emailService;

public RabbitMqEventSubscriber(IConnection connection, IEmailService emailService)
{
_connection = connection;
_emailService = emailService;
}

public void Subscribe()
{
using var channel = _connection.CreateModel();
channel.QueueDeclare(queue: "invoices", durable: false, exclusive: false, autoDelete: false, arguments: null);

var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var invoice = JsonSerializer.Deserialize<Invoice>(message);

if (invoice != null)
{
var emailBody = $@"
<h1>Invoice Generated</h1>
<p>Order ID: {invoice.OrderId}</p>
<p>Product: {invoice.ProductName}</p>
<p>Quantity: {invoice.Quantity}</p>
<p>Price: {invoice.Price:C}</p>
<p>Total: {invoice.Total:C}</p>
<p>Date: {invoice.CreatedAt}</p>";

// replace with your preferred email
await _emailService.SendEmailAsync("customer@example.com", "Your Invoice", emailBody);
Console.WriteLine($"Email sent for invoice: {JsonSerializer.Serialize(invoice)}");
}
};

channel.BasicConsume(queue: "invoices", autoAck: true, consumer: consumer);

Console.WriteLine("Press [enter] to exit.");
Console.ReadLine();
}
}
}

Configuring RabbitMQ in Startup

In the Program.cs file, configure the connection to RabbitMQ and register IEmailService and IEventSubscriber:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NotificationService.Services;
using RabbitMQ.Client;

var builder = WebApplication.CreateBuilder(args);

// Configuring EmailService
builder.Services.AddSingleton<IEmailService, EmailService>();

// Configuring RabbitMQ
builder.Services.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory() { HostName = "localhost" };
return factory.CreateConnection();
});

builder.Services.AddSingleton<IEventSubscriber, RabbitMqEventSubscriber>();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

// Start event subscription service
var subscriber = app.Services.GetRequiredService<IEventSubscriber>();
subscriber.Subscribe();

app.Run();

Updating the Billing Service

Update the billing service to publish invoice events to RabbitMQ:

  1. Adding Dependency: Add the RabbitMQ.Client dependency to the billing project.
  2. Updating the RabbitMqEventSubscriber Class:
using BillingService.Data;
using BillingService.Models;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Text.Json;

namespace BillingService.Services
{
public class RabbitMqEventSubscriber : IEventSubscriber
{
private readonly IConnection _connection;
private readonly IServiceScopeFactory _scopeFactory;

public RabbitMqEventSubscriber(IConnection connection, IServiceScopeFactory scopeFactory)
{
_connection = connection;
_scopeFactory = scopeFactory;
}

public void Subscribe()
{
using (var channel = _connection.CreateModel())
{
channel.QueueDeclare(queue: "orders", durable: false, exclusive: false, autoDelete: false, arguments: null);

var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var order = JsonSerializer.Deserialize<Order>(message);

if (order != null)
{
using (var scope = _scopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<BillingDbContext>();
var invoice = new Invoice
{
Id = Guid.NewGuid(),
OrderId = order.Id,
ProductName = order.ProductName,
Quantity = order.Quantity,
Price = order.Price,
CreatedAt = DateTime.UtcNow
};

context.Invoices.Add(invoice);
await context.SaveChangesAsync();

// Publishing invoice event to RabbitMQ
var invoiceEvent = JsonSerializer.Serialize(invoice);
var invoiceBody = Encoding.UTF8.GetBytes(invoiceEvent);

channel.QueueDeclare(queue: "invoices", durable: false, exclusive: false, autoDelete: false, arguments: null);
channel.BasicPublish(exchange: "", routingKey: "invoices", basicProperties: null, body: invoiceBody);

Console.WriteLine($"Invoice saved and event published: {invoiceEvent}");
}
}
};

channel.BasicConsume(queue: "orders", autoAck: true, consumer: consumer);

Console.WriteLine("Press [enter] to exit.");
Console.ReadLine();
}
}
}
}

Testing the Notification Service

To test the notification service, we need to ensure that all projects (order service, billing service

, and notification service) are started together. Here are the updated steps to configure and run all services together.

Configuring the Solution to Run Multiple Projects

  1. Configure the Solution in Visual Studio:
  • In Solution Explorer, right-click on the solution and select “Properties”.
  • In the “Common Properties” tab, select “Startup Project”.
  • Choose the “Multiple startup projects” option.
  • Set the action to “Start” for each relevant project: OrderService, BillingService, and NotificationService.
  • Click “OK” to save the settings.

Running the Projects

1. Start All Projects:

  • Press F5 in Visual Studio to start all configured projects. Ensure that the OrderService, BillingService, and NotificationService projects are started.

2. Verify Email Sending:

  • Verify that emails are sent correctly when an invoice is generated. Ensure that the order and billing services are publishing and consuming events correctly.

3. Test the API:

  • Use Postman or another tool of your choice to send a POST request to the order service (OrderService).
  • Send a request to the order creation endpoint, for example:
Body (JSON):
{
"ProductName": "Example Product",
"Quantity": 2,
"Price": 100.00
}
  • Verify that the order is created correctly in the order service and the order creation event is published to RabbitMQ.
  • The billing service (BillingService) should consume the order event, create an invoice, and publish the invoice event to RabbitMQ.
  • The notification service (NotificationService) should consume the invoice event and send a notification email.

4. Check Logs and Outputs:

  • Check the logs and outputs of each service to confirm that the messages are being processed correctly and that the notification email was sent.
  • In the console of the NotificationService, you should see a message indicating that the email was successfully sent.

Conclusion

By following these steps, you will be able to test the notification service along with the other services. This ensures that the complete flow, from order creation to email notification, works correctly. If you need anything else or have additional questions, feel free to reach out!

--

--

Fábio Salomão

Developer since 2009 with experience in Microsoft .Net and Full Stack skills. React specialist, focused on creating innovative and efficient solutions.