Razor components within an MVC context
I am currently looking into how to ease our frontend developers with react background into doing more backend work.
Our server code is typically written with the ASP.NET Core tech stack. Sadly, .NET doesn’t tell a particularly good pure server-side render story. cshtml feels ancient at times, no co-located css, no co-located js.
Now, with a few lines of code one can get razor components (which are typically associated with the rather proprietary Blazor tech stack) to kinda play well with the ASP.NET Core MVC stack.
- Add relevant services in
Program.cs
:builder.Services.AddRazorComponents();
- Ability to return razor components from a controller action:
This needs a tiny class which doesn’t seem to exist within ASP.NET Core itself
class ResultToActionResult(IResult result) : IActionResult{ public Task ExecuteResultAsync(ActionContext context) => result.ExecuteAsync(context.HttpContext);}
Now in a controller action, we can basically do:
return new ResultToActionResult( new RazorComponentResult(typeof(TenantList), new Dictionary<string, object> { ["Model"] = result.Other }));
Providing a base class that exposes the provided model as a property makes this work quite similar to a model provided to a view.
Now we are in the space of Razor Components, where component-based UI development can feel much more at home while retaining full control of the HTML sent to the client.
- co-located css: This already works out of the box with razor components,
- co-located js: I found it quite weird that there doesn’t seem to be anything for that. Here, in a POC I took the following approach:
Have a class that is able to hold registrations of js during a request:
// register this at startup like// builder.Services.AddScoped<JavascriptModuleRendering>();public class JavascriptModuleRendering : IEnumerable<string>{ private readonly List<string> scripts = [];
public void Add(Type componentType) { // note that co-located js exists as concept // and will be added to wwwroot in a "publish" operation var fullName = componentType .FullName!.Replace("MyAssembly", "").Replace(".", "/"); scripts.Add($"{fullName}.razor.js"); }
public IEnumerator<string> GetEnumerator() => scripts.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();}
Now, if you, as a component, want to make sure that your co-located js comes to the client, inherit from ComponentWithScriptBase
:
public class ComponentWithScriptBase : ComponentBase{ [Inject] protected JavascriptModuleRendering JavascriptCollector { get; set; } = null!;
protected override void OnInitialized() => JavascriptCollector.Add(GetType());}
Finally, in your layout you can add an “Outlet” for those scripts:
<head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link href="/MyAssembly.styles.css" rel="stylesheet"> <HeadOutlet/> <ScriptsRenderer /></head>
where the ScriptsRenderer.razor looks like this
@using MyAssembly@foreach(var script in JavascriptCollector){ <script src="@script" type="module"></script>}
@code { [Inject] protected JavascriptModuleRendering JavascriptCollector { get; set; } = null!;}
Conclusion
So, with a couple of code pieces, we can leverage Blazor tech to tell a better pure server-side rendering story in ASP.NET Core. My question really is the typical Jurassic Park question:
you were so preoccupied with whether or not you could, you didn’t stop to think if you should.
I’m old enough to have seen a bunch of UI frameworks from Microsoft come and go.
- Should I lean into the Blazor tech in that way or keep living with the inferior cshtml stuff?
- Why does Microsoft not tell a compelling server-side rendering story?
Comments
With an account on the Fediverse or Mastodon, you can respond to this post. Since the Fediverse is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.
Reply to this blog post via mastodon