DEV Community

mohamed Tayel
mohamed Tayel

Posted on • Updated on

What is Clean Architecture: Part 17 - Implementing the Infrastructure Layer with CSV Export and Email Service

Introduction:
In this article, we will build the Infrastructure Layer in a .NET project, focusing on two key functionalities:

  1. Exporting event data to a CSV file using the CsvHelper library.
  2. Sending email notifications when new events are created using the SendGrid service.

By following Clean Architecture principles, we'll ensure that the application remains decoupled from the actual infrastructure implementations. Let’s begin by setting up the project structure and building the infrastructure layer step by step.


1. Setting Up the Infrastructure Layer

The Infrastructure Layer handles external system interactions such as:

  • Sending emails via an external email service (e.g., SendGrid).
  • Exporting data to CSV files for reporting or analytics.

To keep the system maintainable, we separate the contracts (interfaces) into the Application Layer and place the actual implementations into the Infrastructure Layer.

Step 1: Add the Infrastructure Layer to the Solution

  1. In your solution, right-click on the solution name and select Add > New Project.
  2. Choose Class Library as the project type and name it GloboTicket.TicketManagement.Infrastructure.

Image description


2. Implementing CSV Export Functionality

Let’s first implement the CSV export functionality using the CsvHelper library.

Step 2.1: Define the CSV Export Contract in the Application Layer

  1. In your Application Layer, navigate to Contracts/Infrastructure.
  2. Add a new interface ICsvExporter.cs:

Image description

namespace GloboTicket.TicketManagement.Application.Contracts.Infrastructure
{
    public interface ICsvExporter
    {
        byte[] ExportEventsToCsv(List<EventExportDto> eventExportDtos);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2.2: Implement the CsvExporter in the Infrastructure Layer

Now, let’s implement the actual export logic in the Infrastructure Layer.

  1. In your Infrastructure project, create a folder named FileExport.
  2. Add a class named CsvExporter.cs:

Image description

using CsvHelper;
using System.IO;
using System.Text;
using GloboTicket.TicketManagement.Application.Contracts.Infrastructure;
using GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport;

namespace GloboTicket.TicketManagement.Infrastructure.FileExport
{
    public class CsvExporter : ICsvExporter
    {
        public byte[] ExportEventsToCsv(List<EventExportDto> eventExportDtos)
        {
            using var memoryStream = new MemoryStream();
            using (var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8))
            {
                using var csvWriter = new CsvWriter(streamWriter);
                csvWriter.WriteRecords(eventExportDtos);  // Write event records to CSV
            }

            return memoryStream.ToArray();  // Return CSV as byte array
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Purpose: This class converts a list of EventExportDto objects to a CSV format and returns the data as a byte array.

Step 2.3: Create the CSV Export Feature in the Application Layer

Next, we’ll create the handler that will be responsible for exporting events.

  1. In the Application Layer, navigate to Features/Events/Queries/GetEventsExport.

Image description

  1. Add the following files step by step:

EventExportDto.cs:

namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{
    public class EventExportDto
    {
        public Guid EventId { get; set; }
        public string Name { get; set; } = string.Empty;
        public DateTime Date { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

EventExportFileVm.cs:

namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{
    public class EventExportFileVm
    {
        public string EventExportFileName { get; set; } = string.Empty;
        public string ContentType { get; set; } = string.Empty;
        public byte[]? Data { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

GetEventsExportQuery.cs:

using MediatR;

namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{
    public class GetEventsExportQuery : IRequest<EventExportFileVm>
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

GetEventsExportQueryHandler.cs:

using MediatR;
using AutoMapper;
using GloboTicket.TicketManagement.Application.Contracts.Infrastructure;
using GloboTicket.TicketManagement.Domain.Entities;

namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{
    public class GetEventsExportQueryHandler : IRequestHandler<GetEventsExportQuery, EventExportFileVm>
    {
        private readonly IAsyncRepository<Event> _eventRepository;
        private readonly IMapper _mapper;
        private readonly ICsvExporter _csvExporter;

        public GetEventsExportQueryHandler(IMapper mapper, IAsyncRepository<Event> eventRepository, ICsvExporter csvExporter)
        {
            _mapper = mapper;
            _eventRepository = eventRepository;
            _csvExporter = csvExporter;
        }

        public async Task<EventExportFileVm> Handle(GetEventsExportQuery request, CancellationToken cancellationToken)
        {
            var allEvents = _mapper.Map<List<EventExportDto>>((await _eventRepository.ListAllAsync()).OrderBy(x => x.Date));
            var fileData = _csvExporter.ExportEventsToCsv(allEvents);

            return new EventExportFileVm
            {
                ContentType = "text/csv",
                Data = fileData,
                EventExportFileName = $"{Guid.NewGuid()}.csv"
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Implementing Email Functionality Using SendGrid

Next, let’s add email functionality using SendGrid.

Step 3.1: Define the Email Service Contract

  1. In the Application Layer, navigate to Contracts/Infrastructure.
  2. Add a new interface IEmailService.cs:
namespace GloboTicket.TicketManagement.Application.Contracts.Infrastructure
{
    public interface IEmailService
    {
        Task<bool> SendEmail(Email email);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3.2: Implement the Email Service in the Infrastructure Layer

  1. In the Infrastructure project, create a folder called Mail.
  2. Add a new class named EmailService.cs:

Image description

using GloboTicket.TicketManagement.Application.Contracts.Infrastructure;
using GloboTicket.TicketManagement.Application.Models.Mail;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using Microsoft.Extensions.Logging;

namespace GloboTicket.TicketManagement.Infrastructure.Mail
{
    public class EmailService : IEmailService
    {
        private readonly EmailSettings _emailSettings;
        private readonly ILogger<EmailService> _logger;

        public EmailService(IOptions<EmailSettings> mailSettings, ILogger<EmailService> logger)
        {
            _emailSettings = mailSettings.Value;
            _logger = logger;
        }

        public async Task<bool> SendEmail(Email email)
        {
            var client = new SendGridClient(_emailSettings.ApiKey);
            var sendGridMessage = MailHelper.CreateSingleEmail(
                new EmailAddress(_emailSettings.FromAddress, _emailSettings.FromName),
                new EmailAddress(email.To),
                email.Subject,
                email.Body,
                email.Body);

            var response = await client.SendEmailAsync(sendGridMessage);

            _logger.LogInformation("Email sent");

            return response.StatusCode == System.Net.HttpStatusCode.Accepted || response.StatusCode == System.Net.HttpStatusCode.OK;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3.3: Send Email from the Application Layer

Now let’s use the IEmailService in the CreateEventCommandHandler to send an email when an event is created.

  1. Navigate to Features/Events/Commands/CreateEvent.
  2. Open CreateEventCommandHandler.cs and modify the code as follows:

Image description

using AutoMapper;
using GloboTicket.TicketManagement.Application.Contracts.Infrastructure;
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
using GloboTicket.TicketManagement.Application.Models.Mail;
using GloboTicket.TicketManagement.Domain.Entities;
using MediatR;

namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{
    public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, Guid>
    {
        private readonly IEventRepository _eventRepository;
        private readonly IMapper _mapper;
        private readonly IEmailService _emailService;

        public CreateEventCommandHandler(IMapper mapper, IEventRepository eventRepository, IEmailService emailService)
        {
            _mapper = mapper;
            _eventRepository = eventRepository;
            _emailService = emailService;
        }

        public async Task<Guid> Handle(CreateEventCommand request, CancellationToken cancellationToken)
        {
            var @event = _mapper.Map<Event>(request);

            var validator = new CreateEventCommandValidator(_eventRepository);
            var validationResult = await validator.ValidateAsync(request);

            if (validationResult.Errors.Count > 0)
                throw new Exceptions.ValidationException(validationResult);

            @event = await _eventRepository.AddAsync(@event);

            // Sending email notification to admin
            var email = new Email
            {
                To = "admin@example.com",
                Subject = "A new event was created",
                Body = $"A new event was created: {request}"
            };

            try
            {
                await _email

Service.SendEmail(email);
            }
            catch (Exception ex)
            {
                // Log the error and continue
            }

            return @event.EventId;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Registering Services in the DI Container

To complete the setup, you need to register the EmailService and CsvExporter in the DI container.

  1. In the Infrastructure project, navigate to InfrastructureServiceRegistration.cs:

Image description

public static class InfrastructureServiceRegistration
{
    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<EmailSettings>(configuration.GetSection("EmailSettings"));
        services.AddTransient<IEmailService, EmailService>();
        services.AddTransient<ICsvExporter, CsvExporter>();

        return services;
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Make sure to call this method in Program.cs of your API project:
builder.Services.AddInfrastructureServices(builder.Configuration);
Enter fullscreen mode Exit fullscreen mode

Conclusion:

In this article, we successfully created the Infrastructure Layer with support for:

  • CSV export using the CsvHelper library.
  • Email sending using the SendGrid service.

By adhering to Clean Architecture principles, we ensured that both the Application Layer and Infrastructure Layer remain decoupled, making the system maintainable and extendable.
For the complete source code, you can visit the GitHub repository: https://github.com/mohamedtayel1980/clean-architecture

Top comments (0)