Render view from controller in ASP.NET Core

In ASP.NET Core, if you want to render a view to a string, this Stack Overflow answer makes it simple. I’ve used this to return HTML in a JSON request that also includes other data, to build an email body, and to get raw HTML to pass off to wkhtmltopdf to build PDF from HTML.

public static async Task<string> RenderViewAsync<TModel>(this Controller controller, 
    string viewName, TModel model, bool partial = false) {
    if (string.IsNullOrEmpty(viewName)) {
        viewName = controller.ControllerContext.ActionDescriptor.ActionName;
    }

    controller.ViewData.Model = model;

    using (var writer = new StringWriter()) {
        IViewEngine viewEngine = controller.HttpContext.RequestServices
            .GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
        ViewEngineResult viewResult = viewEngine.FindView(
            controller.ControllerContext, viewName, !partial);

        if (viewResult.Success == false) {
            return $"A view with the name {viewName} could not be found";
        }

        ViewContext viewContext = new ViewContext(controller.ControllerContext,
            viewResult.View, controller.ViewData, controller.TempData,
            writer, new HtmlHelperOptions()
        );

        await viewResult.View.RenderAsync(viewContext);

        return writer.GetStringBuilder().ToString();
    }
}

MVC Disable Converting Empty String to Null

Some joker decided that empty strings passed into an MVC controller should be converted to null by default. One way to avoid this is to decorate the string properties of the model class:

using System.ComponentModel.DataAnnotations;

public class SomeModel {
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public string SomeProperty { get; set; }
}

[HttpPost]
public JsonResult DoSomething(SomeModel myModel) {
    bool isEmpty = (myModel.SomeProperty == string.Empty);
    return Json(new { IsEmpty = isEmpty });
}

Then when you pass in an empty string, it will remain an empty string in the .NET code:

jQuery.ajax({
    type: "post",
    url: "@Url.Action("DoSomething")",
    data: JSON.stringify({ SomeProperty: "" }),
    dataType: "json",
    contentType: "application/json;charset=utf-8;"
}).done(function (result) {
    console.log(result);
});

Javascript controller actions

In your ASP.NET MVC application, you may need to reference action URLs, for things like AJAX calls. You can do this inline in CSHTML with something like:

var url = "@Url.Action("SavePerson", "People")";

But it’s a little harder to do it in standalone javascript files.

You can define your app root and then concatenate, like:

<script>
window._rootUrl = "@Url.Content("~")";
</script>
// In standalone .js file:
var url = _rootUrl + "/People/SavePerson";

But that only works if you’re using standard URL routing. And if you try to do it with absolute references, like /People/SavePerson, you might run into issues if your development, test, and production environments are at different levels in IIS.

One solution is to define all your routes in javascript, so you can use them globally. You can use something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;
using Newtonsoft.Json;

namespace Acme.Web {
    /// <summary>
    /// You must initialize this in order to use it. 
    /// Generally you'll want to initialize in your Global.asax.cs
    /// Application_Start method, by calling:    
    /// ActionListController.Initialize(typeof(HomeController).Assembly);
    /// If you have controllers in multiple assemblies, 
    /// pass in an array of assemblies to the Initialize method.
    /// </summary>
    public class ActionListController : Controller {
        private static Assembly[] _controllerAssemblies = new Assembly[0];

        public static void Initialize(params Assembly[] controllerAssemblies) {
            if (controllerAssemblies != null) {
                _controllerAssemblies = controllerAssemblies;
            }
        }

        [HttpGet]
        [OutputCache(Duration = 6000, VaryByParam = "none")]
        public ActionResult ControllerActionList() {
            var rootObjects = new Dictionary<string, object>();

            // Area -> Controller -> Action -> Url
            var areas = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();

            // Controller -> Action -> Url
            var rootMethods = new Dictionary<string, Dictionary<string, string>>();

            foreach (var assembly in _controllerAssemblies) {
                foreach (var controller in assembly.GetTypes().Where(t => typeof (Controller).IsAssignableFrom(t))) {
                    string controllerName = Regex.Replace(controller.Name, "Controller$", string.Empty);
                    var areaNameMatch = Regex.Match(controller.Namespace, @"(?<=\.Areas\.)[a-zA-Z0-9_]+");
                    if (areaNameMatch.Success) {
                        if (!areas.ContainsKey(areaNameMatch.Value)) {
                            areas[areaNameMatch.Value] = new Dictionary<string, Dictionary<string, string>>();
                        }
                        areas[areaNameMatch.Value].Add(controllerName, GetActionsFromController(controller, new {Area = areaNameMatch.Value}));
                    } else {
                        rootMethods.Add(controllerName, GetActionsFromController(controller));
                    }
                }
            }
            foreach (var kvp in areas) {
                rootObjects[kvp.Key] = kvp.Value;
            }
            foreach (var kvp in rootMethods) {
                rootObjects[kvp.Key] = kvp.Value;
            }
            string script = string.Format("window._controllerActions={0};", JsonConvert.SerializeObject(rootObjects).Replace(@"/", @"\/"));
            return Content(script, "text/javascript");
        }

        private Dictionary<string, string> GetActionsFromController(Type controller, object routeValue = null) {
            var controllerActions = new Dictionary<string, string>();
            string controllerName = Regex.Replace(controller.Name, "Controller$", string.Empty);

            var controllerMethods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance);
            var syncActionMethods = controllerMethods.Where(m => typeof (ActionResult).IsAssignableFrom(m.ReturnType));
            var asyncActionMethods = controllerMethods
                .Where(m => m.ReturnType.IsGenericType
                    && m.ReturnType.GetGenericTypeDefinition() == typeof (Task<>)
                    && typeof (ActionResult).IsAssignableFrom(m.ReturnType.GetGenericArguments()[0]));

            foreach (var method in syncActionMethods.Union(asyncActionMethods)) {
                controllerActions[method.Name] = Url.Action(method.Name, controllerName, routeValue);
            }
            return controllerActions;
        }
    }
}

In your layout page, make sure you put that script tag above all others.

<script src="@Url.Action("ControllerActionList", "ActionList", new {v = "1.0"})"></script>

When you’re ready to use it, simply call:

var url = _controllerActions.People.SavePerson;