Rendering (and Emailing) Embedded Razor Views with .NET Core

Home / Rendering (and Emailing) Embedded Razor Views with .NET Core

In continuing my efforts to process emails in .NET Core, I needed a way to send styled emails with images and such. Previously, I would have manually handled this by replacing text here and there, but using Razor Views seemed like a much better alternative. Considering that the processing would be handled in my domain layer, all of the views would be generated from embedded resources in a class library.

Embedded resources in .NET Core are somewhat different than legacy .NET resources. Basically, you can drop files into a class library and set the build type to “embed.” I created an “EmbeddedViews” folder to mimic that of a typical MVC project’s view structure. I also have one image (email-icon) that I plan to display (embed) within the email. This is created as an embedded resource too.

I may have blogged about this before, but I’m not sure. The intent for rendering is to render the Razor view as a string and then email that with MailKit (aka MimeKit). Rendering a Razor view as a string is pretty straight-forward. I have an IViewRenderService interface which is injected into the domain layer.

public interface IViewRenderService
    Task<string> RenderToStringAsync(string viewName);
    Task<string> RenderToStringAsync<TModel>(string viewName, TModel model);
    string RenderToString<TModel>(string viewPath, TModel model);
    string RenderToString(string viewPath);

public class ViewRenderService : IViewRenderService
    private readonly IRazorViewEngine _viewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ViewRenderService(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
        _viewEngine = viewEngine;
        _httpContextAccessor = httpContextAccessor;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;

    public string RenderToString<TModel>(string viewPath, TModel model)
            var viewEngineResult = _viewEngine.GetView("~/", viewPath, false);

            if (!viewEngineResult.Success)
                throw new InvalidOperationException($"Couldn't find view {viewPath}");

            var view = viewEngineResult.View;

            using (var sw = new StringWriter())
                var viewContext = new ViewContext()
                    HttpContext = _httpContextAccessor.HttpContext ?? new DefaultHttpContext { RequestServices = _serviceProvider },
                    ViewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model },
                    Writer = sw
                return sw.ToString();
        catch (Exception ex)
            throw new Exception("Error ending email.", ex);

    public async Task<string> RenderToStringAsync<TModel>(string viewName, TModel model)
        var httpContext = _httpContextAccessor.HttpContext ?? new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using (var sw = new StringWriter())
            var viewResult = _viewEngine.FindView(actionContext, viewName, false);

            // Fallback - the above seems to consistently return null when using the EmbeddedFileProvider
            if (viewResult.View == null)
                viewResult = _viewEngine.GetView("~/", viewName, false);

            if (viewResult.View == null)
                throw new ArgumentNullException($"{viewName} does not match any available view");

            var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                Model = model

            var viewContext = new ViewContext(
                new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                new HtmlHelperOptions()

            await viewResult.View.RenderAsync(viewContext);
            return sw.ToString();

    public string RenderToString(string viewPath)
        return RenderToString(viewPath, string.Empty);

    public Task<string> RenderToStringAsync(string viewName)
        return RenderToStringAsync<string>(viewName, string.Empty);

This renderer works really well. But, there’s no obvious way to retrieve an embedded resource. .NET Core, though, has a built in file provider, through the nuget Package Microsoft.Extensions.FileProviders.Embedded, that will search through an assembly’s embedded resources. We add this in our Startup.cs:

// Add the ViewRenderService
services.AddTransient<IViewRenderService, ViewRenderService>();

// Add the embedded file provider
var viewAssembly = typeof(BaseService).GetTypeInfo().Assembly;
var fileProvider = new EmbeddedFileProvider(viewAssembly);
services.Configure<RazorViewEngineOptions>(options =>

Now, if we wanted to render the embedded Razor view, the ViewRenderService is called directly:

var viewRenderSvc= app.ApplicationServices.ServiceProvider.GetService<IViewRenderService>();
var outputSync = viewRenderSvc.RenderToString("~/EmbeddedViews/Mail/OutgoingMail.cshtml");

One note of interest regarding the embedded Razor views. I didn’t get any particular “ViewStart” to be loaded, so I specified the Layout directly in the view that I’m rendering.

    ViewBag.Title = "Outgoing Message";
    Layout = "~/EmbeddedViews/Shared/_EmbeddedLayout.cshtml";

Thanks to the EmbeddedFileProvider, both the view and the specified Layout are found without incident. With all of that working, how does one go about embedding images so that emails can be displayed properly? Within my Razor view, I have one image that I plan to embed with a hard-coded content Id:

<img class="fix" src="cid:emailicon" width="70" height="70" border="0" alt="" />

This is where things are interesting in dealing with email in MailKit. If you’ve ever used the lod “AlternateViews” in legacy .NET applications, you will probably appreciate how much simpler it is to embed images with MailKit as well.

In order to load an embedded bitmap in .NET Core, we have to get a handle to the resource’s stream. In order to use that data in MailKit, we have to convert it to a byte array.

var mimeMsg = new MimeMessage();

// We need the bitmap as a byte array
var assembly = typeof(BaseService).GetTypeInfo().Assembly;
// If we can't find the resources... we can always check the name
//var resourceNames = assembly.GetManifestResourceNames();
Stream resource = assembly.GetManifestResourceStream("");
Bitmap bmp = new Bitmap(resource);
byte[] imageBytes = null;

using (var ms = new MemoryStream())
    bmp.Save(ms, bmp.RawFormat);
    imageBytes = ms.ToArray();

MailKit requires that we specify the content type and add the byte array as a linked resource. After we create the “BodyBuilder” the LinkedResource is added to the builder. The ContentId is set to match the content Id that was specified in the Razor view.

var builder = new BodyBuilder()
    HtmlBody = body

var contentType = new ContentType("image", $"{GetImageExtension(bmp.RawFormat).Replace(".", string.Empty)}");
var image = builder.LinkedResources.Add("email_icon", imageBytes, contentType);
image.ContentId = "emailicon";
mimeMsg.Body = builder.ToMessageBody();

Here is my helper that gets the file extension (synonymous with the subtype) based on the raw image format.

private string GetImageExtension(ImageFormat format)
    var extension = ImageCodecInfo.GetImageEncoders()
    .Where(ie => ie.FormatID == format.Guid)
    .Select(ie => ie.FilenameExtension
        .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
    return extension;

Finally, with all of the code in place. Our shiny, styled email gets rendered properly. It displays correctly in Outlook as well!

4 thoughts on “Rendering (and Emailing) Embedded Razor Views with .NET Core”

  1. “var viewRenderSvc= app.ApplicationServices.ServiceProvider.GetService();”

    this piece of code seems odd in the context of DI.

    Tried to add this service via DI into controller but seems it isn’t work

    here is my piece of Startup.cs
    public void ConfigureServices(IServiceCollection services)
    // Add the ViewRenderService

    //// Add the embedded file provider
    var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
    var compositeProvider = new CompositeFileProvider(embeddedProvider);

    services.Configure(options =>
    options.AutomaticAuthentication = true;


    1. The line that you quote as seeming odd is purely illustrative. I tossed it into the code sample to show how one could retrieve/executing the rendering directly.

      One problem I see with your DI setup is that you’re not specifying a type for the service to inject.

      In the original code sample, for example, I am injecting the IViewRenderService with the specified implementation:

      // Add the ViewRenderService
      services.AddTransient<IViewRenderService, ViewRenderService>();

      Another issue is that you’re not setting the RazorView options to use the providers that you’ve defined. Of course, if you’re running your process within an MVC context, which I was not, you don’t really need the embedded file provider at all. If you’re not planning to use embedded views, I would remove that section of code entirely.

      Let me know if that helps.

  2. Hey awesome blog post! I’ve got it working, except for partial views.

    Getting this exception:

    Message: System.ArgumentNullException : Value cannot be null.
    Parameter name: member

    On this line of code:

    Got this in my main template:

    @Html.Partial($”~/Views/Footer.cshtml”, Model)

    Any suggestions? 🙂

    1. I keep coming back and looking at that line of code… nothing is jumping out at me unless, someone, the partial path can’t be resolved. I thought I was using views with partials, but it’s possible that I wasn’t. I’ll see if I can reproduce and/or render partials without issue and let you know.

Leave a Reply