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>