-2

I have a c# WebApi project in which the users can make orders in a website, each order after payment complete will execute a function called ConfirmOrder which will update the order status from STB to COMPLETED.

In the following function that looks like this:

public static void ConfirmOrder(string piva, string orderID, double importo = 0, string transazione = "", string paymentID = "", string tipo = "MENU")
{
    string connectionString = getConnectionString(piva);
    using var connection = new MySqlConnection(connectionString);

    string query_menu = "QUERY";
    string query_pagamenti = "QUERY";
    using var cmd = new MySqlCommand(query_pagamenti, connection);
    connection.Open();
    cmd.Parameters.AddWithValue("@tipo", tipo.ToUpper());
    cmd.Parameters.AddWithValue("@importo", importo);
    cmd.Parameters.AddWithValue("@transazione", transazione);
    cmd.Parameters.AddWithValue("@dataOra", DateTime.Now);
    cmd.Parameters.AddWithValue("@orderID", orderID);
    cmd.Parameters.AddWithValue("@paymentID", paymentID);
    cmd.Prepare();
    cmd.ExecuteNonQuery();

    cmd.CommandText = query_menu;
    cmd.ExecuteNonQuery();

    if (!tipo.Equals("MENU"))
    {
        EmailHelper.SendRiepilogo(piva, int.Parse(orderID)); // SENDING SUMMARY MAIL
    }
    
}

I'm calling another function SendRiepilogo which sends a summary to the user and the shop, but in this case i can't wait for that function response but it have to be executed for it's own without stucking ConfirmOrder callback.. so i can't wait for SendRiepilogo to be executed, at this point i've read about IHostingService, but i can't figure out on how i could migrate my SendRiepilogo to a IHostingService and run it from ConfirmOrder...

My SendRiepilogo looks like this:

public static async void SendRiepilogo(string piva, int idOrdine)
{
    var order = GetOrdine(piva, idOrdine);
    if (order == null)
    {
        return;
    }
    try
    {
        var negozio = getNegozio(order.idNegozio);
        var from = new MailAddress("ordini@visualorder.it", "VisualOrder");
        var to = new MailAddress(order.cliente.FirstOrDefault().email);

        using MemoryStream ms = new MemoryStream();
        QRCodeGenerator qrGenerator = new QRCodeGenerator();
        QRCodeData qrCodeData = qrGenerator.CreateQrCode("vo/" + idOrdine, QRCodeGenerator.ECCLevel.Q);
        Base64QRCode qrCode = new Base64QRCode(qrCodeData);
        byte[] byteQr = Convert.FromBase64String(qrCode.GetGraphic(20));
        MemoryStream streamQr = new MemoryStream(byteQr);
        var qrImage = new LinkedResource(streamQr, MediaTypeNames.Image.Jpeg)
        {
            ContentId = "qrImage"
        };
        string nome = order.cliente.FirstOrDefault().nome;
        var orderEmail = new { idOrdine, order, nome, negozio };

        byte[] byteLogo = Convert.FromBase64String(System.Text.Encoding.UTF8.GetString(negozio.logo));
        MemoryStream streamLogo = new MemoryStream(byteLogo);
        var logoImage = new LinkedResource(streamLogo, MediaTypeNames.Image.Jpeg)
        {
            ContentId = "logoImage"
        };



        string template = File.ReadAllText("Views/Emails/EmailRiepilogo.cshtml");
        var htmlBody = Engine.Razor.RunCompile(template, "riepilogo", null, orderEmail);

        AlternateView alternateView = AlternateView.CreateAlternateViewFromString(htmlBody, null, MediaTypeNames.Text.Html);
        alternateView.LinkedResources.Add(qrImage);
        alternateView.LinkedResources.Add(logoImage);


        var message = new MailMessage(from, to)
        {
            Subject = "Riepilogo ordine",
            Body = htmlBody
        };
        message.IsBodyHtml = true;
        message.AlternateViews.Add(alternateView);

        using var smtp = new SmtpClient("smtps.aruba.it", 587)
        {
            EnableSsl = true,
            Credentials = new NetworkCredential("XXX", "XXX")
        };
        await smtp.SendMailAsync(message); // sending email to user
        await smtp.SendMailAsync(MessageNegozio(order, idOrdine, negozio)); // sending email to shop
    }
    catch (Exception e)
    {
        return;
    }

    ConfirmEmail(piva, idOrdine); // setting "EMAIL SENT" flag in DB to true
    return;
}
NiceToMytyuk
  • 3,644
  • 3
  • 39
  • 100
  • 1
    Did you already try `Task.Run(() => SendRiepilogo())` https://stackoverflow.com/a/17805992/575468 – dreijntjens Oct 05 '20 at 08:12
  • @dreijntjens i'm getting `An asynchronous call is already in progress. It must be completed or canceled before you can call this method.`, i've removed await from smtp.SendMailAsync and made function not async to do `Task.Run` – NiceToMytyuk Oct 05 '20 at 08:17
  • @dreijntjens should i keep `await` and `async` while executing Task.Run? – NiceToMytyuk Oct 05 '20 at 08:18
  • Do not mix async and sync, do not use `async void` if it's not an EventHandler (with very rare exceptions). – Fildor Oct 05 '20 at 08:19
  • 2
    You can't fix this with `Task.Run`. The background service is a completely different service, using its own thread. You have to tell it what to do and let it do the job. That's what the docs show btw – Panagiotis Kanavos Oct 05 '20 at 08:19
  • @PanagiotisKanavos actually it's working as expected with `Task.Run` and function in `async void` with await on `smtp.SendMailAsync`, it is a bad practise even if it's working? – NiceToMytyuk Oct 05 '20 at 08:22
  • It's not working. It only seems to be working. `async void` should only be used for asynchronous event handlers. `async void` methods can't be awaited, so there's no guarantee that the objects required by that method will still be alive by the time it executes. The runtime knows nothing about that method (since it's not awaited) and is free to dispose any referenced objects. In ASP.NET Core applications, objects created during a request are disposed when that request ends. There are several SO questions asking why an application throws, crashes or seems to be doing nothing due to `async void` – Panagiotis Kanavos Oct 05 '20 at 08:42

1 Answers1

2

A background (hosted) service is a completely different service, using its own thread to do its job. You can't have your controller "run" something on that service, you have to tell it what to do, and have it do it.

The Background tasks with hosted services section in the docs shows two different ways a long running background service can work :

  • A timed service can run each time a timer fires and do a periodic job, as long as the application is running
  • A queued service waits for messages in a queue and performs a job when a message arrives

Sending an email fits into the second case. You could use the documentation example almost as-is. You can create an IBackgroundTaskQueue interface that clients like your controller can use to submit jobs to run in the background:

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

This interface can be added as a dependency in your container's constructor. Assuming the injected service is called myJobQueue, the controller can enqueue a job to run in the background with :


IBackgroundTaskQueue _myJobQueue

public MyController(IBackgroundTaskQueue myJobQueue)
{
    _myJobQueue=myJobQueue;
}


public void ConfirmOrder(...)
{
    ...
    if (!tipo.Equals("MENU"))
    {
        var ordId=int.Parse(orderID);
    
  
  _myJobQueue.QueueBackgroundWorkItem(ct=>EmailHelper.SendRiepilogoAsync(piva,ordId )); 
}

async void should only be used for asynchronous event handlers. That's not what SendRiepilogo is. async void methods can't be awaited, they are essentially fire-and-forget methods that may never run, as the application doesn't know it has to await them. The correct syntax should be :

public static async Task SendRiepilogoAsync(string piva, int idOrdine)
{
...
}

The rest of the documentation example can be used as-is.

Simplifying the service

Instead of a generic queued service that runs any available job, you could create a queue that accepts specific message classes only, only an address and order ID, and have the service do the job of retrieving any data and sending the email. Essentially, SendRiepilogoAsync becomes part of the background service. This allows creating services that could eg batch emails, send several emails concurrently, apply throttling etc.

This would allow reusing expensive resources or perform expensive operations just once, eg create the SmptClient and authenticate before starting to process queue messages

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236