تهیه قالب برای ارسال ایمیل‌ها در ASP.NET Core توسط Razor Viewها
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

برای ارسال متن ایمیل‌ها، یا می‌توان یک سری رشته را با هم جمع زد و ارسال کرد و یا یک View را به همراه ViewModel آن، طراحی و سپس این View را تبدیل به یک رشته کرد. روش دوم هم قابلیت طراحی بهتری دارد و هم نگهداری و توسعه‌ی آن ساده‌تر است. در ادامه روش تبدیل Razor Viewهای ASP.NET Core را به یک رشته، بررسی می‌کنیم.


تهیه سرویسی برای رندر کردن Razor Viewها به صورت یک رشته

در ادامه کدهای کامل سرویسی را که توسط RazorViewEngine کار رندر کردن یک View و تبدیل آن‌را به رشته انجام می‌دهد، ملاحظه می‌کنید:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Threading.Tasks;
using System;
 
namespace WebToolkit
{
    public static class RazorViewToStringRendererExtensions
    {
        public static IServiceCollection AddRazorViewRenderer(this IServiceCollection services)
        {
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<IViewRendererService, ViewRendererService>();
            return services;
        }
    }
 
    public interface IViewRendererService
    {
        Task<string> RenderViewToStringAsync(string viewNameOrPath);
        Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model);
    }
 
    /// <summary>
    /// Modified version of: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
    /// </summary>
    public class ViewRendererService : IViewRendererService
    {
        private readonly IRazorViewEngine _viewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;
        private readonly IHttpContextAccessor _httpContextAccessor;
 
        public ViewRendererService(
                    IRazorViewEngine viewEngine,
                    ITempDataProvider tempDataProvider,
                    IServiceProvider serviceProvider,
                    IHttpContextAccessor httpContextAccessor)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
            _httpContextAccessor = httpContextAccessor;
        }
 
        public Task<string> RenderViewToStringAsync(string viewNameOrPath)
        {
            return RenderViewToStringAsync(viewNameOrPath, string.Empty);
        }
 
        public async Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model)
        {
            var actionContext = getActionContext();
 
            var viewEngineResult = _viewEngine.FindView(actionContext, viewNameOrPath, isMainPage: false);
            if (!viewEngineResult.Success)
            {
                viewEngineResult = _viewEngine.GetView("~/", viewNameOrPath, isMainPage: false);
                if (!viewEngineResult.Success)
                {
                    throw new FileNotFoundException($"Couldn't find '{viewNameOrPath}'");
                }
            }
 
            var view = viewEngineResult.View;
            using (var output = new StringWriter())
            {
                var viewDataDictionary = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                };
 
                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    viewDataDictionary,
                    new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                    output,
                    new HtmlHelperOptions());
                await view.RenderAsync(viewContext).ConfigureAwait(false);
                return output.ToString();
            }
        }
 
        private ActionContext getActionContext()
        {
            var httpContext = _httpContextAccessor?.HttpContext;
            if (httpContext != null)
            {
                return new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor());
            }
 
            httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }
    }
}
توضیحات:
اصل این کد متعلق است به مایکروسافت در اینجا. اما در کدهای فوق سه قسمت آن بهبود یافته‌است:
الف) به سازنده‌ی کلاس، سرویس IHttpContextAccessor نیز تزریق شده‌است تا بتوان به HttpContext و اطلاعات آن دسترسی یافت. حالت پیش فرض آن، استفاده از new DefaultHttpContext است. در این حالت اگر در قالب‌های ایمیل‌های خود از Url.Action استفاده کنید، استثنای index out of range مربوط به یافت نشدن مسیریابی‌ها را دریافت خواهید کرد. علت اینجا است که new DefaultHttpContext حاوی اطلاعات مسیریابی درخواست جاری سیستم نیست. به همین جهت توسط IHttpContextAccessor در متد getActionContext، کار مقدار دهی قسمت مسیریابی صورت گرفته‌است.
ب) در کدهای مثال اصلی، فقط viewEngine.FindView ذکر شده‌است. این متد حالت‌های یافتن Viewهایی را به صورت FolderName/ViewName، پشتیبانی می‌کند. اگر بخواهیم یک مسیر کامل را مانند "Areas/Identity/Views/EmailTemplates/_RegisterEmailConfirmation.cshtml/~" ذکر کنیم، کار نمی‌کند. به همین جهت در ادامه، بررسی viewEngine.GetView نیز اضافه شده‌است تا این نقصان را پوشش دهد.
ج) یک overload اضافه‌تر هم جهت رندر یک View بدون مدل نیز به آن اضافه شده‌است.


روش استفاده‌ی از سرویس ViewRenderer

اسمبلی که این سرویس در آن تعریف می‌شود باید دارای وابستگی‌های ذیل باشد:
{ 
    "dependencies": {
        "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.1.0",
        "Microsoft.AspNetCore.Mvc.Razor": "1.1.0"
    }
}
سپس در متد ConfigureServices کلاس آغازین برنامه، سرویس‌های مورد نیاز را اضافه کنید:
public void ConfigureServices(IServiceCollection services)
{
   services.AddRazorViewRenderer();
}
کار متد AddRazorViewRenderer، افزودن سرویس‌های IViewRendererService و همچنین IHttpContextAccessor است.
پس از ثبت سرویس‌های مورد نیاز، اکنون می‌توان سرویس IViewRendererService را به سازنده‌ی کنترلرها و یا کلاس‌های برنامه تزریق و از متدهای RenderViewToStringAsync آن استفاده کرد:
public class RenderController : Controller
{
    private readonly IViewRendererService _viewRenderService;
    public RenderController(IViewRendererService viewRenderService)
    {
        _viewRenderService = viewRenderService;
    }
 
    public async Task<IActionResult> RenderInviteView()
    {
        var viewModel = new InviteViewModel
        {
            UserId = "1",
            UserName = "Vahid"
        };
        var emailBody = await _viewRenderService.RenderViewToStringAsync("EmailTemplates/Invite", viewModel).ConfigureAwait(false);
        //todo: send emailBody
        return Content(emailBody);
    }
}
برای مثال در اینجا در قالب Invite (یا فایل invite.cshtml) واقع در پوشه‌ی EmailTemplates، جهت ساخت متن ایمیل استفاده شده‌است.


چند نکته‌ی تکمیلی در مورد قالب‌های ایمیل

- پیش فرض این سرویس، یافتن Viewها در پوشه‌ی Views است؛ مانند: Views\EmailTemplates\_EmailsLayout.cshtml
مگر اینکه مسیر آن‌را به صورت کامل توسط filename.cshtml/.../~ ذکر کنید و در این حالت ذکر پسوند فایل الزامی است.
- ایمیل‌ها می‌توانند دارای Layout هم باشند. برای مثال فایل Views\EmailTemplates\_EmailsLayout.cshtml را با محتوای ذیل ایجاد کنید:
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Language" content="fa" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style type='text/css'>
     .main {
            font-size: 9pt;
            font-family: Tahoma;
     }
    </style>
</head>
<body bgcolor="whitesmoke" style="font-size: 9pt; font-family: Tahoma; background-color: whitesmoke; direction: rtl;">
   <div class="main">@RenderBody()</div>
</body>
</html>
در اینجا RenderBody@ را مشاهده می‌کنید که محل رندر شدن ایمیل‌های برنامه است.
به علاوه در اینجا جهت ارسال ایمیل‌ها باید هر نوع شیوه نامه‌ای، به صورت صریح قید شود (inline css) و نباید فایل css ایی را لینک کنید.
- پس از اینکه فایل layout خاص ارسال ایمیل‌های خودتان را طراحی کردید، اکنون قالب یکی از ایمیل‌های برنامه، یک چنین فرمتی را پیدا می‌کند که Layout در ابتدای آن ذکر شده‌است:
 @using Sample.ViewModels
@model RegisterEmailConfirmationViewModel
@{
Layout = "~/Views/EmailTemplates/_EmailsLayout.cshtml";
}
با سلام
<br />
 اکانت شما با مشخصات ذیل ایجاد گردید:
....
- حتما تولید لینک‌ها را به صورت مطلق و نه نسبی انجام دهید. اینکار توسط قید صریح protocol صورت می‌گیرد:
 <a style="direction:ltr" href="@Url.Action("Index", "Home", values: new { area = "" }, protocol: this.Context.Request.Scheme)">@Model.EmailSignature</a>
  • #
    ‫۷ سال قبل، دوشنبه ۶ شهریور ۱۳۹۶، ساعت ۱۳:۱۹
    با سلام؛ اگر بخوایم ایمیل با متن فارسی بفرستیم و خب ساختار بوت استرپ داشته باشه توی مرورگرها من غالبا به مشکل خوردم . خیلی از موارد رو به صورت inline تعریف کردم اما فونت و یه مقدار ساختار رو نمی‌شه و نیاز به فایل اکسترنال دارن . راه حل شما چیه ؟
    • #
      ‫۷ سال قبل، دوشنبه ۶ شهریور ۱۳۹۶، ساعت ۱۴:۰۵
      راه حل جامعی ندارد؛ چون تمام mail clients از آن پشتیبانی نمی‌کنند. اما آنهایی که از آن پشتیبانی می‌کنند به صورت ذیل است:
      - ابتدا تعریف فونت وب به صورت لینک شده از محلی مشخص؛ به همراه فونت fallback برای آوت لوک:
      <style type="text/css">
        @font-face{
          font-family:'Open Sans';
          font-style:normal;
          font-weight:400;
          src:local('Open Sans'), local('OpenSans'), url('http://fonts.gstatic.com/s/opensans/v10/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff') format('woff');
        }
      </style>
      
      <!--[if mso]>
      <style type="text/css">
      .fallback-text {
          font-family: Arial, sans-serif;
      }
      </style>
      <![endif]-->
      - سپس استفاده‌ی از آن:
      <td class="fallback-text" style="font-family: 'Open Sans', Arial, sans-serif;">Open sans font for all!</td>
  • #
    ‫۶ سال و ۵ ماه قبل، چهارشنبه ۲۹ فروردین ۱۳۹۷، ساعت ۱۸:۵۳
    خلاصه نکات این مطلب در برنامه‌های ASP.NET Core

    ابتدا بسته‌ی نیوگت DNTCommon.Web.Core را نصب کنید:
    PM> Install-Package DNTCommon.Web.Core
    سپس مثالی از IViewRendererService آن‌را در اینجا می‌توانید مشاهده کنید.   
  • #
    ‫۴ سال و ۱۰ ماه قبل، دوشنبه ۲۰ آبان ۱۳۹۸، ساعت ۲۲:۵۹
    با سلام؛ خطای زیر از چی میتونه باشه؟
    System.AggregateException: 'Some services are not able to be constructed' 
    InvalidOperationException: Error while validating the service descriptor 'ServiceType: Blog.Core.Services.Interfaces.IUserService Lifetime: Transient ImplementationType: Blog.Core.Services.UserService': Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'Blog.Core.Convertors.ViewRendererService'. 
    InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'Blog.Core.Convertors.ViewRendererService'.
    در ضمن پروژه از نوع asp.net core 3 هست.
    • #
      ‫۴ سال و ۱۰ ماه قبل، سه‌شنبه ۲۱ آبان ۱۳۹۸، ساعت ۰۰:۱۶
      - روش معرفی MVC به ASP.NET Core 3.0 اندکی تغییر کرده؛ اطلاعات بیشتر
      - در این حالت برای مثال اگر فقط AddControllers را اضافه کنید، موتور Razor را ثبت نمی‌کند. به همین جهت خطای یافت نشدن سرویس IRazorViewEngine را دریافت می‌کنید. این خطا با اضافه کردن ()services.AddMvc برطرف می‌شود.
  • #
    ‫۳ سال قبل، پنجشنبه ۴ شهریور ۱۴۰۰، ساعت ۲۲:۵۲
    درود
    آیا امکان دارد کد داخل فایل view را بصورت string (بعنوان مثال از بانک اطلاعاتی یا ساخته شده بصورت دستی ) مانند همین کد رندر کنیم .
    مثال :
    کد html ای بصورت string داریم که در بانک اطلاعاتی ذخیره شده است :
    @model string
    <div>@Model</div>
    <div><CutumTagHelper asp-id="1000" /></div>
    حال در قسمتی از View اصلی میخواهیم این مقدار string خوانده شده از بانک را ابتدا بصورت یک view مجزا رندر و نتیجه آن را بصورت Html نمایش دهیم :
    بجای
    ...
    
    @Html.Raw(Model.HtmlData)
    
    ....
    چنین کدی داشته باشیم :

    ...
    
    @Html.SomeNameLikeRenderedRaw(Model.HtmlData,Model.SomeDataAsDynamicViewModel)
    
    ....

      • #
        ‫۳ سال قبل، شنبه ۶ شهریور ۱۴۰۰، ساعت ۱۶:۲۹
        با تشکر از شما این مقاله را دیده بودم ولی مشکل من را حل نکرد . شاید بشه بهتر بگم که در :
        var viewEngineResult = _viewEngine.FindView(actionContext, viewNameOrPath, isMainPage: false);
                    if (!viewEngineResult.Success)
                    {
                        viewEngineResult = _viewEngine.GetView("~/", viewNameOrPath, isMainPage: false);
                        if (!viewEngineResult.Success)
                        {
                            throw new FileNotFoundException($"Couldn't find '{viewNameOrPath}'");
                        }
                    }
        
                    var view = viewEngineResult.View;
        بشه از :
        var view = CreateViewFromString(@"@model int <div class="AAA">@Model</div>");
        استفاده کرد.

        • #
          ‫۳ سال قبل، شنبه ۶ شهریور ۱۴۰۰، ساعت ۱۷:۴۹
          از کتابخانه‌هایی مانند «RazorLight » استفاده کنید.  
          • #
            ‫۳ سال قبل، شنبه ۶ شهریور ۱۴۰۰، ساعت ۲۰:۲۱
            سپاس
            متاسفانه از TagHelper و ViewComponent‌ها پشتیبانی نمیکند