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 theStartNew
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 theTask.Run
method by default are created with theTaskCreationOptions.DenyChildAttach
option. To override this behavior, or to provide otherTaskCreationOptions
options, call aStartNew
overload.- Parameter passing. The overloads of the
Task.Run
method do not allow you to pass a parameter to the task delegate. Overloads of theStartNew
method do.- The task scheduler. The overloads of the
Task.Run
method use the default task scheduler. To control the task scheduler, call aStartNew
overload with a scheduler parameter. For more information, seeTaskScheduler
.
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);