2

I have an ASP.NET MVC project that uses Razor for rendering HTML using the built-in engine.

I would like to create Email Templates using the same process. Usually, these templates are created as part of a Action Context (for instance, when a user has completed a purchase, a notification would be sent). However, there are some instances where there is no context available. For instance, sending a log when the application is restarting.

This is what I have come up with so far:

public static string RenderRazor(ViewTemplateType TemplateType, string ViewName, PageController Controller = null, object Model = null)
{
    try
    {
        ControllerContext Context;

        if (Controller != null)
        {
            Context = Controller.ControllerContext;
        }
        else
        {
            if (HttpContext.Current == null)
            {
                throw new InvalidOperationException("Cannot render a razor template if the current context is null (See DEV-1669).");
            }
            var RouteData = new RouteData();
            RouteData.Values.Add("controller", "Pseudo");
            Controller = new PseudoController(Model);
            Context = new ControllerContext(new HttpContextWrapper(HttpContext.Current), RouteData, Controller);
            // If this isn't set, an error occurs when calling FindView/FindViewPartial.
            Controller.ControllerContext = Context;
        }

        // I'm not really sure what the point of this is...
        // Further, it was actually causing an exception to occur since the Controller may not actually be populated?
        // Without this, the Notification Debug wasn't working - so apparently it is required in some circumstances for notifications.
        if (Controller != null && Controller.ViewData != null && Model != null) { Controller.ViewData.Model = Model; }

        var ViewResult = ViewName.StartsWith("_")
            ? ViewEngines.Engines.FindPartialView(Context, string.Format("~/Views/Template/{0}/{1}.cshtml", TemplateType, ViewName))
            : ViewEngines.Engines.FindView(Context, string.Format("~/Views/Template/{0}/{1}.cshtml", TemplateType, ViewName), string.Format("~/Views/Template/{0}/_Shared/{1}.cshtml", TemplateType, "_Layout"));

        if (ViewResult.View == null)
        {
            StringBuilder LocationBuilder = new StringBuilder();
            string[] SearchedLocations = ViewResult.SearchedLocations.ToArray();
            for (int i = 0; i < SearchedLocations.Length; i++)
            {
                LocationBuilder.Append(string.Format("({0}) {1} ", i, SearchedLocations[i]));
            }

            throw new InvalidOperationException(string.Format("Could not find the {0} Template named {1} using the {2} Master. Locations Searched: {3}", TemplateType, ViewName, "_Layout", LocationBuilder));//
        }
        using (var Writer = new StringWriter())
        {
            //ViewResult.View.Render(new ViewContext(
            //  Context,
            //  ViewResult.View,
            //  (Controller != null) ? Controller.ViewData : (Model != null) ? new ViewDataDictionary(Model) : new ViewDataDictionary(),
            //  (Controller != null) ? Controller.TempData : new TempDataDictionary(), Writer), Writer);

            ViewResult.View.Render(new ViewContext(
                Context,
                ViewResult.View,
                Controller.ViewData,
                Controller.TempData, Writer), Writer);

            ViewResult.ViewEngine.ReleaseView(Context, ViewResult.View);

            // This must remove Tabs (\t) Returns (\r) and Newlines (\n)
            // Always making the quotes single makes sense for statically generated stuff - the only time when it wouldn't make sense is for more complex stuff or if it includes JS which i don't think
            // a statically generated one should ever?

            string result = Regex.Replace(Writer.GetStringBuilder().ToString().Replace("\"", "\'"), "(\\t|\\r|\\n)", string.Empty);

            // Currently, this process does not work well when initiated outside of a request (e.g. in the startup method or purely within a
            // hangfire task). This serves as a warning if it ever is (since it will return an empty string).

            if (result.Blank()) { throw new InvalidOperationException("There was an error rendering the " + ViewName + " template. This can happen if the template was initialized outside of the context of an actual request."); }
            else
            {
                return result;
            }
        }
    }
    catch (Exception ex)
    {
        // This could indicate an error in the underlying template
        // If there is an error on any of the underlying templates in a given class,
        // this can be called.
        Logging.Error(ex);
        return string.Empty;
    }
}

This works pretty well except for when I am trying to generate a template when I don't have a reference to a Controller.

I have also started looking into applying the third-party RazorEngine (https://antaris.github.io/RazorEngine) - but is this overkill? Is it even feasible to implement this in a project that also utilizes the built-in razor engine?

1 Answer 1

1

I've used ActionMailer in the past. It's no longer maintained but there's a fork https://github.com/crossvertise/ActionMailerNext.

You set up a controller to bind the templates but can call it like another service class.

public class EmailController : MailerBase, IEmailService
{
    public EmailResult PasswordRecovery(PasswordRecoveryModel model)
    {
        To.Add(model.Email);
        From = "[email protected]";
        Subject = "Password Recovery";
        return Email("PasswordRecovery", model);
    }
}

I set it up with my own interface so I can inject it wherever

public interface IEmailService
{
    EmailResult PasswordRecovery(PasswordRecoveryModel model);
}


public class Foo
{
    private readonly IEmailService emailService;

    public Foo(IEmailService emailService)
    {
        this.emailService = emailService;
    }

    public void DoSomething()
    {
        this.emailService.PasswordRecovery(new PasswordRecoveryModel { ... });
    }
}

The templates look just like regular views

PasswordRecovery.html.cshtml

@using ActionMailer.Net.Mvc
@model PasswordRecoveryModel

<p>@Model.UserName</p>
<div> ... </div>

I haven't tried the fork so I can only assume it's similar in usage.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.