Home > Back-end >  Which TPL suits best for this scenario in .NET API ? Task/Threads
Which TPL suits best for this scenario in .NET API ? Task/Threads

Time:10-05

I have this method that increases the views of a video when it's watched. If the video of the person who published it reaches 200 views, I send an congrats email to him, but if the email sending DELAYS, I don't want the user to wait for that delay to watch the video. I need to make Video loading not depend on email sending. I have used Task.Run and Task.Factory.StartNew and inside it Parallell I don't have experience with tasks/threads so which is the best way to implement it.

public async Task<ServiceResponse<ViewedVideoDto>> AddVideoView(int candidateVideoId)
        {
            var video = await _dbcontext.CandidateFiles.Include(x=>x.Candidate).ThenInclude(x=>x.User).FirstOrDefaultAsync(x => x.Id == candidateVideoId);
            if (video is null)
            {
                return new ServiceResponse<ViewedVideoDto>().NotFound(nameof(Resource.VideoEmpty), Resource.VideoEmpty);
            }
            video.VideoViews  = 1;
            _dbcontext.Update(video);
            await _dbcontext.SaveChangesAsync();
            if (video.VideoViews == 200)
            {
                //Method 1 
                await Task.Run(() =>
                   _candidateEmailService.Achieved200VideoViews(video.Candidate.User.Email,
                       video.Candidate.User.FullName));

                //Method 2
                   Task.Factory.StartNew(() => Parallel.Invoke(() =>
                  {
                      _candidateEmailService.Achieved200VideoViews(video.Candidate.User.Email,
                          video.Candidate.User.FullName);
                  }));
                  
            }
            return new ServiceResponse<ViewedVideoDto>()
            {
                Data = new ViewedVideoDto()
                {
                    VideoId = video.Id
                }
            };
            

        }

 public async Task Achieved200VideoViews(string receiverEmail, string fullName)
    {
        var candidateTemplate = EmailTemplate.Return(EmailTemplateConst.CanEmailTempFolder, EmailTemplateConst.CanAchieved200VideoViews,fullName);
            
        var innoEmail = new InnoEmail()
        {
            Email = receiverEmail,
            Subject = "Touchpoint after 200 views",
            HtmlMessage = candidateTemplate 
        };
        await _emailService.SendAsync(innoEmail);
public async Task<bool> SendAsync(Email email)
        {
            try
            {
                var apiKey = _configuration.GetSection("SENDGRID_API_KEY").GetSection("SecretKey").Value;
                var Email = _configuration.GetSection("SENDGRID_API_KEY").GetSection("Email").Value;

                var client = new SendGridClient(apiKey);
                var msg = new SendGridMessage()
                {
                    From = new EmailAddress(Email, "test"),
                    Subject = email.Subject,
                    PlainTextContent = null,
                    HtmlContent = email.HtmlMessage
                };
                msg.AddTo(new EmailAddress(email.Email));

                var response = await client.SendEmailAsync(msg).ConfigureAwait(false);

                response.StatusCode.ToString();
                return true;
            }
            catch (System.Exception e)
            {
                return false;
            }
        }

CodePudding user response:

The Method 1 (await Task.Run(() =>) will not achieve the declared goal cause you are awaiting the result which basically will suspend the AddVideoView execution until the Achieved200VideoViews finishes. If you want fire-and-forget behaviour - remove the await.

Based on Achieved200VideoViews implementation, basically it just sends mail - I would say that it does not matter which one is better cause both fire-and-forget approaches are not good enough, cause they lack observability (for example email sending can fail for multiple reasons or app will be restarted during the process).

I would suggest and approach similar to transactional outbox pattern - add either a flag to video table or separate table which will store info about notification about 200 views send (something like a video.ToSend200ViewNotification) and update that in AddVideoView and then process that table in some background job handler for example using hosted services or Hangouts or Quartz (or add flag Is200ViewNotificationSend and process all videos with views >= 200 and Is200ViewNotificationSend set to false).

If you are ok with limitations of fire-and-forget approach for synchronous operations Task.Run should be preferable (without await). Task.Factory.StartNew, as written in the docs TaskFactory.StartNew is more suitable for more complicated scenarios (also it is not task-aware and in "ordinary" cases will not handle methods returning Task's correctly):

Starting with the .NET Framework 4.5, the Task.Run method is the recommended way to launch a compute-bound task. Use the StartNew method only when you require fine-grained control for a long-running, compute-bound task. This includes scenarios in which you want to control the following:

  • Task creation options. Tasks created by the Task.Run method by default are created with the TaskCreationOptions.DenyChildAttach option. To override this behavior, or to provide other TaskCreationOptions options, call a StartNew overload.
  • Parameter passing. The overloads of the Task.Run method do not allow you to pass a parameter to the task delegate. Overloads of the StartNew method do.
  • The task scheduler. The overloads of the Task.Run method use the default task scheduler. To control the task scheduler, call a StartNew overload with a scheduler parameter. For more information, see TaskScheduler.

But since your Achieved200VideoViews method already returns Task and assuming it is "truly async" then just skipping await on the invokation should be fine to achieve fire-and-forget functionality (and I would say it is preferable cause it should be less resource-consuming):

_ = _candidateEmailService.Achieved200VideoViews(video.Candidate.User.Email,
                       video.Candidate.User.FullName);
  • Related