0

I am building website with a donation system for a charity as a pro bono project. After a user gives a memorial donation, a tribute PDF is to be generated and sent to the recipient.

I have a unit test for PdfService.GenerateMemorialPdf(...) which works fine. However, when testing my event handler via my task queue, which in turn calls the aforementioned method, the debugging session breaks abruptly when the PDF is to be generated. I cannot find any exceptions.

If I modify the test to call ExecuteAsync directly (rather than the event handler's Handle()) then the test completes. So I think the issue is connected to my test strategy with respect to the task queue.

I’m a junior developer with a couple of months of experience.

Any help would be much appreciated!

The code

The event handler is an implementation of my EnqueueingEventHandler, which enqueues ExecuteAsync(). The taskQueue is essentially the same as described in Microsoft Learn.

namespace CharityName.Application.Events;

public abstract class EnqueueingEventHandler<TEvent>(IBackgroundTaskQueue taskQueue) : IEventHandler<TEvent>
    where TEvent : IEvent
{
    public Task Handle(TEvent eventRequest, CancellationToken cancellationToken)
    {
        taskQueue.QueueBackgroundWorkItemAsync((CancellationToken) => ExecuteAsync(eventRequest, CancellationToken));
        return Task.CompletedTask;
    }

    protected abstract ValueTask ExecuteAsync(TEvent eventRequest, CancellationToken cancellationToken);
}

Here is the implementation of the donation created event handler, which currently only generates a PDF and sends it to the recipient via email. This is the method being tested (test follows in the end).

namespace CharityName.Application.Events.DonationCreatedEvent;

public class DonationCreatedEventHandler(
    ILogger<DonationCreatedEventHandler> logger,
    IPdfService pdfService,
    IEmailService emailService,
    IBackgroundTaskQueue taskQueue)
    : EnqueueingEventHandler<DonationCreatedEvent>(taskQueue)
{
    protected override async ValueTask ExecuteAsync(DonationCreatedEvent eventRequest, CancellationToken cancellationToken)
    {
        logger.LogInformation("Event {event} is being dequeued from the taskqueue", typeof(DonationCreatedEvent));
        string? pdf = null;
        if (eventRequest.DonationDto.DonationType == Domain.Enums.DonationType.Memorial)
        {
            pdf = pdfService.GenerateMemorialPdf(eventRequest.LetterDto.FullName, eventRequest.LetterDto.Message, PdfImage.heart_filled);
        }

        var res = await emailService.SendRecieptAsync(
            EmailTemplate.reciept,
            eventRequest.DonorDto.Email,
            eventRequest.DonorDto.FirstName,
            pdf,
            "Temporary");

        return;
    }
}

Here is the PdfService. Note the comment indicating where the debugging session crashes. Remember, this method works fine in unit test.

namespace CharityName.Infrastructure.Services.PdfService;

public class PdfService: IPdfService
{
    private const string BaseImagePath = "Services/PdfService/Resources/";

    public string GenerateMemorialPdf(string honoreeName, string message, PdfImage image)
    {
        // Temporary code, this does not throw
        var imagePath = BaseImagePath + image + ".png";
        if (!File.Exists(imagePath))
        {
            throw new FileNotFoundException($"Image file not found at path: {imagePath}");
        }

        var documentModel = new MemorialDocumentModel()
        {
            HonoreeName = honoreeName,
            Message = message,
            Image = new Image() { Location = BaseImagePath + image + ".png"}
        };

        var document = new MemorialDocument(documentModel);

        var pdf = document.GeneratePdf(); // <--- debug session ends here

        return Convert.ToBase64String(pdf);
    }
}

Here's the test

namespace CharityName.Tests.IntegrationTests
{
    public class DonationCreatedEventHandlerTests
    {

        private readonly EmailService _emailService;
        private readonly PdfService _pdfService;
        private readonly BackgroundTaskQueue _taskQeueue;
        private readonly QueuedHostedService _queuedHostedService;
        private readonly IConfiguration _configuration;

        public DonationCreatedEventHandlerTests()
        {

            _configuration = new ConfigurationBuilder()
                .AddUserSecrets<DonationCreatedEventHandlerTests>()
                .Build();

            var brevoHttpClient = BrevoClientTestHelper.Create(_configuration);

            _emailService = new EmailService(brevoHttpClient, new Mock<ILogger<EmailService>>().Object);
            _pdfService = new PdfService();
            _taskQeueue = new BackgroundTaskQueue(100);
            _queuedHostedService = new QueuedHostedService(_taskQeueue, new Mock<ILogger<QueuedHostedService>>().Object);
            QuestPDF.Settings.License = LicenseType.Community;
        }

        [Fact]
        public async Task Test1()
        {
            // Arrange
            await _queuedHostedService.StartAsync(CancellationToken.None);

            var handler = new DonationCreatedEventHandler(
                new Mock<ILogger<DonationCreatedEventHandler>>().Object,
                _pdfService,
                _emailService,
                _taskQeueue);

            var donationDto = new DonationDto()
            {
                Amount = 5000,
                DonationType = Domain.Enums.DonationType.Memorial,
                Token = Guid.NewGuid()
            };

            var donorDto = new DonorDto()
            {
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]"
            };

            var letterDto = new LetterDto()
            {
                LetterType = Domain.Enums.LetterType.Celebratory,
                DeliveryMethod = Domain.Enums.DeliveryMethod.email,
                LatestsDeliveryDate = DateTime.Now,
                FullName = "John Doe",
                Email = "[email protected]",
                Message = "Rest in Peace"
            };

            var @event = new DonationCreatedEvent(donationDto, donorDto, letterDto);

            // Act
            await handler.Handle(@event, CancellationToken.None);

            // Assert
            Assert.True(true); // to be changed
        }
    }
}

Update:

Might as well add the taskQueue and hosted service.

using System.Threading.Channels;
using CharityName.Application.Abstractions.Interfaces;

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}
using CharityName.Application.Abstractions.Interfaces;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger) : BackgroundService
{
    public IBackgroundTaskQueue TaskQueue { get; } = taskQueue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Queued Hosted Service is running.");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
10
  • 2
    What debugger are you using? What does the output window say about why the process ended?
    – Richard
    Commented Oct 31, 2024 at 9:23
  • 1
    Next step: debug into MemorialDocument (not part of QuestPDF AFAICS). This might be something in the internals of QuestPDF (which appears to be a wrapper around an underlying QuestPdfSkia which is included as a dll) which likely means you may need to enable native debugging and disable "just my code" options.
    – Richard
    Commented Oct 31, 2024 at 11:22
  • 1
    Also check the debug output to ensure the underlying dll is loaded OK.
    – Richard
    Commented Oct 31, 2024 at 11:22
  • 1
    (I am assuming your unit tests avoid integrations, so are faking the PDF generation.)
    – Richard
    Commented Oct 31, 2024 at 11:23
  • 1
    I would suggest thinking less about how you call things, than focusing on what you call. That is a failure of some time in MemorialDocument: focus inside that. But as the warning about extended discussion notes; this is very specifically "how do I debug a problem" and that is something that needs interactive help (which is beyond the effort I do for free).
    – Richard
    Commented Oct 31, 2024 at 17:12

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.