Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • ewuski 99 posts 286 karma points
    Mar 20, 2024 @ 20:03
    ewuski
    0

    Transactional email in a loop not working

    Can the transactional service handle sending transactional emails in a loop?

    We are trying to send a transactional email to multiple customers:

    Option 1

    var customersToEmail = await GetCustomers();
    
    foreach (var customer in customersToEmail)
    {
       _transactionalEmailService.SendReminderEmail(customer);
    }
    

    and then:

    public CommandResult<SendTransactionalEmailResponse> SendReminderEmail(ReminderEmailModel customer)
    {
        return _newsletterStudioService.SendTransactional(
            SendTransactionalEmailRequest.Create(customer)
                .SendTo(customer.Email)
                .WithSubject(customer.Subject)
                .Build()
        );
    }
    

    First email goes out and then the consecutive email fails with the error:

    System.InvalidOperationException: Context flow is already suppressed. at System.Threading.ExecutionContext.SuppressFlow() at NewsletterStudio.Core.Public.NewsletterStudioService.SendTransactional(SendTransactionalEmailRequest request)

    Option 2

    If we try to execute it in async mode then the first email gets sent tout and the consecutive email hits the campaign service instead of the transactional.

    var customersToEmail = await GetCustomers();
    
    foreach (var customer in customersToEmail)
    {
        await _transactionalEmailService.SendReminderEmail(customer);
    }
    

    and then:

    public async Task<CommandResult<SendTransactionalEmailResponse>> SendReminderEmail(ReminderEmailModel customer)
    {
        return await Task.FromResult(_newsletterStudioService.SendTransactional(
            SendTransactionalEmailRequest.Create(customer)
                .SendTo(customer.Email)
                .WithSubject(customer.Subject)
                .Build())
        );
    }
    

    Umbraco 10.8.3 NS 10.0.8

  • Markus Johansson 1936 posts 5864 karma points MVP 2x c-trib
    Mar 21, 2024 @ 09:38
    Markus Johansson
    0

    Hi!

    Are you running this in a background job in some way?

    I need to look closer at why you're getting the "Context flow is already suppressed."-error but a potential workaround for you is to force the sending to be sequential by chaining on the .NotAsync() call when building up the SendTransactionalEmailRequest.

    Update your "Option 1" so look something like this:

    return await Task.FromResult(_newsletterStudioService.SendTransactional(
        SendTransactionalEmailRequest.Create(customer)
            .SendTo(customer.Email)
            .WithSubject(customer.Subject)
            .NotAsync()
            .Build())
    

    Let me know if this works as a workaround for your problem?

    Cheers!

  • ewuski 99 posts 286 karma points
    Mar 29, 2024 @ 14:10
    ewuski
    0

    Hi, No, it didn't help.

    If I add .NotAsync() I am getting the following on the first send (no matter within async or sync method):

    System.InvalidOperationException
      HResult=0x80131509
      Message=Cannot resolve scoped service 'Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers.IViewBufferScope' from root provider.
      Source=Microsoft.Extensions.DependencyInjection
      StackTrace:
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
       at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
       at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
       at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
       at Microsoft.AspNetCore.Mvc.Razor.RazorView.<RenderAsync>d__18.MoveNext()
       at NewsletterStudio.Web.Rendering.RazorHostViewRenderer.<RenderToStringAsync>d__6.MoveNext()
       at NewsletterStudio.Web.Rendering.RazorHostViewRenderer.RenderPartialViewToString(String viewName, Object model)
       at NewsletterStudio.Core.Rendering.EmailRenderer.Render(IRecipientDataModel recipient)
       at NewsletterStudio.Core.Sending.Transactional.TransactionalEmailSender.Send(SendTransactionalEmailRequest request)
    
  • Markus Johansson 1936 posts 5864 karma points MVP 2x c-trib
    Mar 29, 2024 @ 15:02
    Markus Johansson
    0

    Hi!

    Can you explain more about your use case please?

    Are you sending from inside a HTTP-request or are you doing this in some kind of background job? If not, are you using a async method to send?

    Please explain the process so that I understand the context.

    If you don't feel comfortable sharing inner details here, please reach out over email markus {at sign goes here} enkelmedia.se.

  • ewuski 99 posts 286 karma points
    Apr 08, 2024 @ 13:18
    ewuski
    0

    Hi Markus, It is an API call that calls the service where is the loop I quoted in my first post.

    I tried both with async and without it.

  • Markus Johansson 1936 posts 5864 karma points MVP 2x c-trib
    Apr 08, 2024 @ 13:56
    Markus Johansson
    0

    Hi!

    I need to understand the exact details around this, what do you mean with "API call"?

    Where exactly are you hosting this code?

    var customersToEmail = await GetCustomers();
    
    foreach (var customer in customersToEmail)
    {
       _transactionalEmailService.SendReminderEmail(customer);
    }
    

    Is it a "regular" API Controller like this:

    [ApiController]
    public class MyController : Controller 
    {
       public ActionResult Something()
       {
           var customersToEmail = await GetCustomers();
    
          foreach (var customer in customersToEmail)
          {
             _transactionalEmailService.SendReminderEmail(customer);
          }
    
       }
    }
    

    Could you provide some more exact details around how/where you are calling the service?

    Are you using anything "special" framework? Minimal API, API Controllers? Fast Endpoints? Please provide as much details as possible so that I can replicate the issue.

    The thing is that it looks like the INewsletterStudioService is used in context where the dependency injection container does not have a scope and the rendering engine is dependent on e.g. IViewBufferScope which is a scoped dependency. The fact that the error says "....from root provider." indicates that there is something about the context is which the service is used that is causing the issue - that's why I need as much details as possible.

    Cheers!

  • ewuski 99 posts 286 karma points
    May 07, 2024 @ 18:20
    ewuski
    0

    Hi Markus, It is UmbracoApiController. It's a pretty basic call atm because we just started developing it.

    The controller action gets customer emails and then processes them in foreach loop that calls the ITransactionalEmailService and its SendReminderEmail() action, the one that I pasted initially. That's it.

    We have a scheduled task that runs the following action:

    public class ReminderApiController : UmbracoApiController
    {
        private readonly ITransactionalEmailService _transactionalEmailService;
    
        public ReminderApiController(ITransactionalEmailService transactionalEmailService)
        {
            _transactionalEmailService = transactionalEmailService;
        }
    
        [HttpGet]
        public async Task<IActionResult> SendReminder(string subjectTemplate)
        {
            var customersToEmail = await GetCustomers();
    
            foreach (var customer in customersToEmail)
            {
                _transactionalEmailService.SendReminderEmail(customer);
            }
    
            ...
        }
    }
    

    It happens locally. We only started developing this feature to try NS' transactional emails.

    Not sure what else you wanna know?

  • ewuski 99 posts 286 karma points
    May 07, 2024 @ 19:01
    ewuski
    0

    I have simplified it for you so hopefully you'll be able to replicate it:

    Controller:

    public class ReminderApiController : UmbracoApiController
    {
        private readonly IMediator _mediator;
    
        public ReminderApiController(IMediator mediator)
        {
            _mediator = mediator;
        }
    
        [HttpGet]
        public async Task<IActionResult> SendReminder(string subjectTemplate = "Reminder")
        {
            var query = new ReminderHandler.Query(subjectTemplate);
    
            var response = await _mediator.Send(query);
    
            return Ok(JsonConvert.SerializeObject(response));
        }
    }
    

    Handler:

    internal class ReminderHandler
    {
        #region Query
        public record class Query(string SubjectTemplate) : IRequest<Result>;
    
        public class Result
        {
            public List<string> Errors { get; set; }
            public bool Success { get; set; }
        }
        #endregion
    
        #region Handler
        public class QueryHandler : IRequestHandler<Query, Result>
        {
            #region Private properties
            private readonly ITransactionalEmailService _transactionalEmailService;
            #endregion
    
            #region Constuctor
            public QueryHandler(
                ITransactionalEmailService transactionalEmailService
            )
            {
                _transactionalEmailService = transactionalEmailService;
            }
            #endregion
    
            #region Handler
            public async Task<Result> Handle(Query request, CancellationToken cancellationToken)
            {
                var result = new Result();
                var errors = new List<string>();
    
                try
                {
                    var customersToEmail = await GetCustomers(request.SubjectTemplate);
    
                    foreach (var customer in customersToEmail)
                    {
                        _transactionalEmailService.SendReminderEmail(customer);
                    }
    
                    result.Success = true;
                }
                catch (Exception ex)
                {
                    errors.Add(ex.Message);
                }
    
                result.Errors = errors;
    
                return result;
            }
            #endregion
    
            private async Task<List<ReminderEmailModel>> GetCustomers(string subjectTemplate)
            {
                var customers = new List<ReminderEmailModel>
                {
                    new ReminderEmailModel()
                    {
                        EncodedRef = "ref 1",
                        ProductTitle = "Title 1",
                        FirstName = "Mary",
                        Email = "[email protected]",
                        DestinationCountry = "Spain",
                        Subject = subjectTemplate
                    },
                    new ReminderEmailModel()
                    {
                        EncodedRef = "ref 2",
                        ProductTitle = "Title 2",
                        FirstName = "John",
                        Email = "[email protected]",
                        DestinationCountry = "Turkey",
                        Subject = subjectTemplate
                    },
                    new ReminderEmailModel()
                    {
                        EncodedRef = "ref 3",
                        ProductTitle = "Title 3",
                        FirstName = "John",
                        Email = "[email protected]",
                        DestinationCountry = "Greece",
                        Subject = subjectTemplate
                    }
                };
    
                return customers;
            }
        }
        #endregion
    }
    

    Model:

    [TransactionalEmail("Reminder", Alias)]
    public class ReminderEmailModel
    {
        public const string Alias = "reminder";
    
        [MergeField("Encoded Ref", "encodedRef")]
        public string EncodedRef { get; set; }
    
        [MergeField("Product Title", "productTitle")]
        public string ProductTitle { get; set; }
    
        [MergeField("First Name", "firstName")]
        public string FirstName { get; set; }
    
        public string DestinationCountry { get; set; }
    
        public string Subject { get; set; }
    
        public string Email { get; set; }
    }
    

    Service:

    public class TransactionalEmailService : ITransactionalEmailService
    {
        private readonly INewsletterStudioService _newsletterStudioService;
    
        public TransactionalEmailService(
            INewsletterStudioService newsletterStudioService
            )
        {
            _newsletterStudioService = newsletterStudioService;
        }
    
        public CommandResult<SendTransactionalEmailResponse> SendReminderEmail(ReminderEmailModel customer)
        {
            return _newsletterStudioService.SendTransactional(
                SendTransactionalEmailRequest.Create(customer)
                    .SendTo(customer.Email)
                    .WithSubject(customer.Subject)
                    .Build()
            );
        }
    }
    

    The transactional template exists in the BO with the reminder model selected.

    The above code hits the mentioned error on returning SendTransactional.

    The first email gets through and gets logged, but the following ones don't due to the error.

    We now use NS version 10.0.13. The same error as in the version 10.0.8 persists.

  • Markus Johansson 1936 posts 5864 karma points MVP 2x c-trib
    May 07, 2024 @ 21:35
    Markus Johansson
    100

    Hi!

    Thank you very much for a informative message with lot of details!

    I managed to replicate the issue when using a async controller like in your example, I also managed to get around it by not sending the emails using a new thread by adding a NotAsync() call when creating the request.

    By default the service will fire of a new thread to send a email (kind of like fire and forget). This behavior might introduce problems if you send emails in a tight loop, but adding NotAsync() will send the email in a sequential way.

    This code should work:

    var res = _newsletterStudioService.SendTransactional(
          SendTransactionalEmailRequest.Create(customer)
          .SendTo(customer.Email)
          .WithSubject(customer.Subject)
          .NotAsync()
          .Build());
    

    I've uploaded my demo-project here so that you can verify that it works for you as well:

    https://www.dropbox.com/scl/fi/hivl4d2fibvse5vorssk2/Umbraco-Newsletter-Studio-Transactional-Loop.zip?rlkey=w0icx4sanvk59k0o6mjzuxcy1&dl=0

    I've done everything in the same way as you are showing here (using MediatR etc) but also added .NotAsync(). Can you spot any difference between what you are doing and how this project works?

    A couple of questions:

    1. I'm starting to wonder if this could be related to something about the project setup? Is this a "regular" Umbraco website? Do you have MVC referenced in the project? (It's a dependency to render the e-mails razor views). The error about IViewBufferScope that you posted indicates that the rendering pipeline can't access this dependency which is needed to render the e-mail.

    2. Do you have anything "special" inside the email content? Macros that does something fancy etc? If yes, let me know more details.

    3. Just to be sure, I'm assuming that the rendering works in the backoffice?

    4. You mention a "scheduled task" that calls this controller. What kind of scheduled task is this? The old Umbraco-concept of a scheduled task was removed in v9, do you use a background job, Hangfire or a Scheduled Task in Windows that call this endpoint, or how does it work?

    EDIT:

    I've also published a new version v10.0.14 where there is a check before trying to SuppressFlow on the ExecutionContext, this should mean that the call without NotAsync() should work as well.

    https://www.nuget.org/packages/NewsletterStudio/10.0.14

  • ewuski 99 posts 286 karma points
    May 08, 2024 @ 17:23
    ewuski
    0

    Adding NotAsync() as per our instruction above still throws the error I mentioned earlier:

    System.InvalidOperationException HResult=0x80131509 Message=Cannot resolve scoped service 'Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers.IViewBufferScope' from root provider. Source=Microsoft.Extensions.DependencyInjection
    StackTrace: at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
    at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at Microsoft.AspNetCore.Mvc.Razor.RazorView.

    However, the upgrade to 10.0.14 fixed the issue and we can now send transactional emails in the loop.

    We've detected other issues though, related to inserting a content link that I described here: https://our.umbraco.com/packages/backoffice-extensions/newsletter-studio-the-email-studio/comments//114081-issues-inserting-a-content-link-with-params-in-email-template

  • Markus Johansson 1936 posts 5864 karma points MVP 2x c-trib
    May 08, 2024 @ 20:55
    Markus Johansson
    0

    Hi!

    I can't replicate the problem with the IOC not being able to resolve the IViewBufferScope dependency.

    Did you try the solution that I provided?

    Would be very helpful if you could take a minute or two and answer the numbered questions that might give me some insights into what is different/wrong here.

    It's probably some kind of difference between a "vanilla umbraco project" and the project that you're running.

    I'm happy to hear that the fixes in 10.0.14 made the synchronous approach work for you, since your sending as a background job this is probably acceptable since no user needs to wait for it.

    I'll address your other issues in the other thread.

    / Markus

Please Sign in or register to post replies

Write your reply to:

Draft