2
\$\begingroup\$

I am working on an e-commerce website. In this specific scenario, people can upload their CV to apply for a job. The job application is sent to ApplyForJob action method:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<JsonResult> ApplyForJob(JobApplicationViewModel jobApplicationViewModel)
{
    if (ModelState.IsValid)
    {
        // store the CV as the user's most recent CV inside his specific folder (S3 bucket)
        _jobApplicationService.UpdateUserJobProfile(jobApplicationViewModel);

        var adHeadline = GetAdHeadlineOrRedirectWithWarning(jobApplicationViewModel.AdBaseId);

        // build JobApplicationEmail model
        JobApplicationEmail jobApplicationDetails = new JobApplicationEmail(
            jobApplicationViewModel.ApplicantFullName,
            jobApplicationViewModel.ApplicantEmail,
            adHeadline.UserProfileViewModel.FirstName,
            adHeadline.GetBestContactEmail(),
            adHeadline.Title,
            ShoplessUrlHelper.GetCanonicalUrl(adHeadline.GetRelativeLink()),
            jobApplicationViewModel.MessageHtmlString,
            jobApplicationViewModel.CvByteArray,
            jobApplicationViewModel.CvFileName,
            jobApplicationViewModel.ApplicantPhoneNumber,
            jobApplicationViewModel.ApplicantLinkedinUrl);

        // send the email to the employer
        await _emailService.SendEmailAsync(jobApplicationDetails);
        return Json(new { Success = "true" });
    }

    return Json(new { Success = "false" });
}

This is the JobApplicationViewModel. As you can see the CvFile is uploaded in HttpPostedFileBase. This model has two additional getters, CvByteArray and CvFileName, to get the details of uploaded CV file:

public class JobApplicationViewModel : ContactBaseViewModel
{
    private string _applicantLinkedinUrl;
    private byte[] _cvBytes = null;

    public JobApplicationViewModel()
    {
        AdBaseId = 0;
    }

    public JobApplicationViewModel(long adBaseId, string applicantFullName, string applicantEmail)
    {
        AdBaseId = adBaseId;
        ApplicantFullName = applicantFullName;
        ApplicantEmail = applicantEmail;
    }

    public long ApplicantUserId { get; set; }

    [Required]
    [Display(Name = "Full name")]
    [StringLength(GlobalConstants.MaxLengthForFullName, ErrorMessage = "{0} must be between {2} and {1} characters long", MinimumLength = GlobalConstants.MinLengthForFullName)]
    public string ApplicantFullName { get; set; }  // FullName that was used in prev job application and can be different from user's account FirstName/LastName

    [Required]
    [Display(Name = "Email")]
    [EmailAddress]
    public string ApplicantEmail { get; set; }     // Email that was used in prev job application, this can be different from user's account email

    [Display(Name = "Phone number")]
    public string ApplicantPhoneNumber { get; set; }

    [Display(Name = "LinkedIn URL")]
    [StringLength(GlobalConstants.MaxLengthForUrl)]
    [RegularExpressionWithOptions(GlobalConstants.LinkedinValidationRegEx, ErrorMessage = "Not a valid LinkedIn URL", RegexOptions = RegexOptions.IgnoreCase)]
    public string ApplicantLinkedinUrl
    {
        get
        {
            return ShoplessUrlHelper.PrependHttpsIfMissing(_applicantLinkedinUrl);
        }

        set
        {
            _applicantLinkedinUrl = value;
        }
    }

    [Display(Name = "CV")]
    [RegularExpressionWithOptions(GlobalConstants.CvFileNameRegEx, ErrorMessage = "Invalid file format, use: doc, docx, pdf, odt or txt", RegexOptions = RegexOptions.IgnoreCase)]
    public string UploadedCvFileName { get; set; } 

    public string ExistingCvFileName { get; set; } // <-- The CV file name from prev job application, it comes from DB (not from user)

    /// <summary>
    /// Use CvByteArray and CvFileName properties for getting these values
    /// </summary>
    public HttpPostedFileBase CvFile { get; set; }

    public byte[] CvByteArray
    {
        get
        {
            if (_cvBytes != null)
            {
                return _cvBytes;
            }

            using (Stream inputStream = CvFile.InputStream)
            {
                MemoryStream memoryStream = inputStream as MemoryStream;
                if (memoryStream == null)
                {
                    memoryStream = new MemoryStream();
                    inputStream.CopyTo(memoryStream);
                }

                _cvBytes = memoryStream.ToArray();
            }

            return _cvBytes;
        }
    }

    public string CvFileName
    {
        get
        {
            return Path.GetFileName(CvFile.FileName);
        }
    }
}

This is the JobApplicationEmail model... the main thing about this model is that cv (the byte array) is copied into Attachment and cvFileName is copied to AttachmentFileName

[Serializable]
public class JobApplicationEmail : EmailBase
{
    private static string _inlineHtmlTemplate = "Email's HTML template, this string contains serveral -PlaceHolders-";

    private StringBuilder _htmlBuilder;

    public JobApplicationEmail(string fromFullName, string fromEmail, string toFirstName, string toEmail, string adTitle, string adLink, string messageHtmlString, byte[] cv = null, string cvFileName = "", string phoneNumber = "", string linkedinUrl = "")
    : base()
    {
        _htmlBuilder = new StringBuilder(_inlineHtmlTemplate);
        ToFullName = ToDisplayName = toFirstName;
        ToEmail = toEmail;
        FromDisplayName = fromFullName;
        FromFullName = EmailConfig.MailerFullName;
        FromEmail = EmailConfig.MailerEmail;
        ReplyToEmail = fromEmail;
        ReplyToFullName = fromFullName;
        Attachment = cv;
        AttachmentFileName = cvFileName;
        Subject = $"Job Application from {fromFullName}";
        _htmlBuilder.Replace("-applicantFullName-", fromFullName);
        _htmlBuilder.Replace("-adTitle-", adTitle);
        _htmlBuilder.Replace("-adLink-", adLink);
        _htmlBuilder.Replace("-messageHtmlString-", string.IsNullOrWhiteSpace(messageHtmlString) == false ? messageHtmlString : "The applicant has not included any message");
        _htmlBuilder.Replace("-fromEmail-", fromEmail);

        if (string.IsNullOrEmpty(phoneNumber))
        {
            _htmlBuilder.Replace("-phoneNumberDisplayValue-", "none");
        }
        else
        {
            _htmlBuilder.Replace("-phoneNumberDisplayValue-", "block");
            _htmlBuilder.Replace("-phoneNumber-", phoneNumber);
            _htmlBuilder.Replace("-phoneNumberNoSpace-", phoneNumber.RemoveAllWhiteSpaces());
        }

        if (string.IsNullOrEmpty(linkedinUrl))
        {
            _htmlBuilder.Replace("-linkedinDisplayValue-", "none");
        }
        else
        {
            _htmlBuilder.Replace("-linkedinDisplayValue-", "block");
            _htmlBuilder.Replace("-linkedinUrl-", linkedinUrl);
            _htmlBuilder.Replace("-linkedinProfileDisplayText-", "LinkedIn profile");
        }

        HtmlContent = _htmlBuilder.ToString();
    }
}

Now, there are 2 actions which need to take place on the CV:

  1. The CV file should be stored in user's folder (inside an S3 bucket) so that the user can reuse it for his next application)

  2. The CV should be attached to an email and be sent to the employer

I want to make this process as efficient as possible, that's why I am converting the CV to a byte array inside the model and pass this byte array to both of the above methods:

  1. Save CV inside S3 bucket... I have not included all the function calls, but this is the final step where CV gets stored inside user's folder inside S3 bucket... this code is called from _jobApplicationService.UpdateUserJobProfile:
public string UploadCv(byte[] cvBytes, string cvFileName, long userId)
{
    var userCvFolderRelativePath = SecureDocumentsFolderHelper.GetUserCvFolderRelativePath(userId);
    var cvfileNameRelativePath = S3PathMapper.CombineHighLevelPath(userCvFolderRelativePath, cvFileName);

    // if it's the first time this user is applying for a job, create a folder for him
    S3DirectoryInfo di = new S3DirectoryInfo(_cdnClient, _cdnBucketName, cvfileNameRelativePath);
    if (!di.Exists)
    {
        di.Create();
    }
    else
    {
        ClearFolder(userCvFolderRelativePath);
    }

    // copy the CV as the most up to date version
    S3FileInfo s3FileInfo = new S3FileInfo(_cdnClient, _cdnBucketName, cvfileNameRelativePath);
    if (!s3FileInfo.Exists)
    {
        using (var s3Stream = s3FileInfo.Create())
        {
            s3Stream.Write(cvBytes, 0, cvBytes.Length);
        }
    }

    return cvFileName;
}
  1. Here is the second step which is called from _emailService.SendEmailAsync, sends the email to the employer.
if (email.Attachment != null && !string.IsNullOrEmpty(email.AttachmentFileName))
{
    using (WebClient webClient = new WebClient())
    {
        var base4File = Convert.ToBase64String(email.Attachment);
        sendGridMessage.AddAttachment(email.AttachmentFileName, base4File);
    }
}

As you can email.Attachement is the byte array which is converted to Base64String and is added to the email...

I am not sure if is a more efficient way of dealing with CV file? The CV file is uploaded inside HttpPostedFileBase class and this class has an Stream property... I am not sure if it would be more efficient to just pass the Stream property to the above methods? i.e. directly use the Stream for writing to S3 and email attachment, instead of converting it to byte array?

\$\endgroup\$
1
  • \$\begingroup\$ Consider HtmlAgilityPack NuGet package to build or parse HTML, it's much faster than regular string operations. Also the email template can be const string or stored in a file and loaded once. \$\endgroup\$ – aepot Sep 2 '20 at 22:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.