Smart MVC Razor Pages

Driven by our preference for API-first style of Web Development we've developed our own ServiceStack Razor Pages which lets you develop dynamic Web Pages using Razor to generate the HTML view of your existing Services - saving you from maintaining a parallel Controller implementation that's limited to just Web Pages. The benefits of an API-first approach is that you'll naturally get a well-defined servicified interface which can be consumed by all consumers including Web, Native Mobile and Desktop Apps whilst also enabling simplified B2B Integrations, Automation, Integration testing, etc.

RazorFormat Usage

You can find .NET Core Razor features documented in razor.netcore.io which is maintained in our MVC NuGet package that can be installed with:

<PackageReference Include="ServiceStack.Mvc" Version="8.*" />

Then to enable, register the RazorFormat plugin:

public override void Configure(Container container)
{
    Plugins.Add(new RazorFormat());
}

No Ceremony Dynamic Pages without Controllers

In a lot of cases where you're not developing Web Forms accepting User Input (e.g. generating a dynamic page using simple page-specific db queries) you won't need a Controller or Service at all. For this scenario we developed Controller-less Razor Pages, where if you specify a Typed @model, ServiceStack automatically populates it from the HTTP Request Params and when no @model exists ServiceStack instead populates the Request params in a ViewDataDictionary - in both cases letting you access any Request Params using @Model.Name notation.

Razor Pages also lets you layout your Razor Views in whatever structure you want under /wwwroot which it will let you call using Pretty URLs by default so you're not led into following the MVC-specific {Controller}/{Action} pattern or made to define Custom Routes.

.NET Core Razor Pages implementation also lets you structure your Razor Pages under /Views/Pages as an alternative to maintaining them under /wwwroot.

MVC Razor Pages

Unfortunately in .NET Core we weren't able to reuse any of our existing ServiceStack.Razor implementation, but as we found the development model and end-user experience of Razor without MVC Controllers and Actions much more productive we investigated how it could best be implemented in .NET Core. Unfortunately .NET Core's Razor support is tightly coupled to MVC's implementation, but fortunately for us MVC also provided the necessary APIs where we could re-implement ServiceStack.Razor's user-facing features using just MVC Razor Views.

In many ways this turned out to be a blessing in disguise as by using MVC's implementation we also get access to new MVC .NET Core features and its surrounding ecosystem like Tag Helpers. MVC also takes care of live-reloading Razor Views behind-the-scenes so we're also able to get the same iterative development experience we're used to. By using MVC Views we also naturally get good tooling support which can be a dark art in .NET 4.5 which was tightly coupled to Web.config configuration and therefore poorly supported in Self-Hosting Console Apps.

INFO

Currently ReSharper's tooling has issues with Razor Views inheriting Custom base classes - which can be resolved by installing the latest EAP or disabling its ASP.NET Razor support

Overall we're ecstatic with the end-result, we retain our Controller-free development model whilst Razor under .NET Core executes noticeably quicker than ASP.NET and significantly faster on Linux vs using Mono.

Page Based Routing

Another value-added feature of ServiceStack.Razor is support for Page Based Routing in ASP.NET Core Razor which lets you use a _ prefix to declare a variable placeholder for dynamic routes defined solely by directory and file names.

With this feature we can use a _id directory name to declare an id variable place holder:

This will let you navigate to the edit.cshtml page directly to edit a contact using the ideal "pretty url" we want:

Placeholders can be on both directory or file names, e.g:

  • /contacts/edit/_id.cshtml -> /contacts/edit/1

Inside your Razor page you can fetch any populated placeholders from the ViewBag:

var id = int.Parse(ViewBag.id);
var contact = Html.Exec(() => Gateway.Send(new GetContact { Id = id }).Result, out var error);

Which /_id/edit.cshtml uses to call the GetContact Service using the Service Gateway.

INFO

Html.Exec() is a UX-friendly alternative to using try/catch boilerplate in Razor

Stand-alone Razor Views

Rendering stand-alone HTML Views from Razor Pages can use the GetViewPage() API for retrieving View Pages (e.g. under ~/Views) and the GetContentPage() API for retrieving Content Pages (e.g. under /wwwroot).

You can then use RenderToHtmlAsync() API to render the HTML output in a UTF-8 ReadOnlyMemory<char> which your Services can return directly for optimal efficiency, or if needed the rendered output can be converted to a string with .ToString():

public async Task<object> Any(MyRequest request)
{
    var razor = GetPlugin<RazorFormat>();
    var view = razor.GetViewPage("MyView");
    if (view == null)
        throw HttpError.NotFound("Razor view not found: " + "MyView");

    var ret = await razor.RenderToHtmlAsync(view, new MyModel { Name = "World" },
        layout:"_MyLayout"); //if Layout specified in `.cshtml` page it uses that
    return ret;
}

For even better efficiency the Razor View can render to the Response OutputStream directly with WriteHtmlAsync() to write the rendered UTF-8 bytes directly to the OutputStream instead of above where it converts it into a UTF-8 string before converting it back to UTF-8 bytes when ServiceStack writes it to the response:

public async Task Any(MyRequest request)
{
    var razor = GetPlugin<RazorFormat>();
    var view = razor.GetViewPage("MyView");
    if (view == null)
        throw HttpError.NotFound("Razor view not found: " + "MyView");

    await razor.WriteHtmlAsync(Response.OutputStream, view, 
        new MyModel { Name = "World" }, 
        layout:"_MyLayout"); //if Layout specified in `.cshtml` page it uses that
}

If needed you can also render the view with an anonymous Model Type, e.g:

await razor.RenderToHtmlAsync(view, new { Name = "World" });

Where the Razor View would need to specify it's using a dynamic model with:

@model dynamic

Limitation

One drawback of page based routing is that MVC is unable to resolve Page Based Routes when pre-compiled and will need to disabled with:

.NET Core 3+

<RazorCompileOnPublish>false</RazorCompileOnPublish>

.NET Core 2.x

<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>