Razor.Templating.Core
Razor.Templating.Core copied to clipboard
New Feature: Check if template exists
It would be useful to expose a away to determine if a template exists or not. Instead of changing the RazorTemplateEngine
, a new type could be introduced that encapsulates the logic in RazorViewToStringRenderer.FindView
I have a use case where I need to generate emails. Parts of an email that may need rendering are:
- Subject
- Plain Text Body
- HTML Body
Some emails may have plain text, some may have html and some may have both. Being safely (without throwing an exception) able to check if a template exists can make this use case easier. I could create a naming convention like:
/views/<template>/subject.cshtml
/views/<template>/text.cshtml
/views/<template>/html.cshtml
Below is a basic non-working example of how this could be used
class EmailContent
{
public string Subject { get; set; }
public string? Text { get; set; }
public string? Html { get; set; }
}
class EmailTemplate
{
public EmailContent RenderEmailContent(string templateName, object data)
{
if (!TemplateExists(templateName, "subject")) throw ...
EmailContent content = new EmailContent();
content.Subject = RenderTemplate(templateName, "subject", data);
if (TemplateExists(templateName,"text")) content,Text = RenderTemplate(templateName, "text", data);
if (TemplateExists(templateName,"html")) content,Html = RenderTemplate(templateName, "html", data);
if (string.IsNullOrEmpty(content,Text) && (string.IsNullOrEmpty(content,Html)) throw ...
return content;
}
}
var data = ....
EmailTemplate template = ...
EmailContent content = template.RenderEmailContent("ValidateEmail", data);
For this purpose, we could introduce an API using out
param:
Task<bool> TryRenderAsync(string viewName, out string? renderedView, object? viewModel = null, Dictionary<string, object>? viewBagOrViewData = null)
You can use it like:
if (await RazorTemplateEngine.TryRenderAsync("/Views/ExampleView.cshtml", out var renderedView))
{
// do something with renderedView
}
Does it address your use case?
This could work, but we wound need to ensure the only reason it doesnt render is because the template doesn't exist. If there is a problem rendering it because of a bug (NullReferenceException I am looking at you), then those exceptions should still flow through. I wouldn't want it just to fail and return false because of any random error.
Ok, then we need to be more explicit on the method name. TryRenderIfViewExistsAsync()
would be more appropriate. What do you think?
Would it make sense to create a ViewNotFoundException
? This exception could inherit from in current InvalidOperationException to maintain backward compatibility. We would change the non "try" version to throw ViewNotFoundException. This helps to distinguish between the library unable to find the view and the user doing something that will throw InvalidOperationException (ie accessing the .Value
of a nullable int).
Then keeping the original .TryRenderAsync(viewname, ...)
function. Then the FindView function could be refactored or adjusted to be able to try to find the view. If not found, then allow it to throw ViewNotFoundException.
I think the original signature you proposed is a lot cleaner. If we can just adjust the behavior internally to avoid throwing exception internally in the correct flows. Also the ViewNotFoundException
could make it easier to use.
That's a good idea! I'll think through it & get back to you.
Just to let you know that I've also stumbled over this. It would be handy to have a way of determining if a view exists or not instead of catching InvalidOperationException
and checking if its Message
starts with Unable to find view
which is brittle.
Thanks for the shout @dradovic! I've been busy all these days due to a job change and other personal commitments. But I'll try to spend some time on this over the coming days or weeks.
Unrelated comment - thanks for a great library! 🥇
I guess this isn't release yet, or is it?
Hey @dradovic, this is not released yet. Required changes are already done but needs some minor cleanup. I'll probably do it this week.
Hey @dradovic @pbolduc
I've published a pre-release package. If anyone gets a chance please try it and let me know how it works for you. https://github.com/soundaranbu/Razor.Templating.Core/releases/tag/v2.0.0-rc.1
The approach is pretty much similar to what @pbolduc suggested and there's a slight change in the contract. I'm returning a tuple because the out param is not supported in the async method.
Thanks for the support <3
I can't use it due to:
NU1202: Package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 9.0.0-preview.1.24081.5 is not compatible with net8.0 (.NETCoreApp,Version=v8.0). Package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 9.0.0-preview.1.24081.5 supports: net9.0 (.NETCoreApp,Version=v9.0)
It's too early for me to switch to .NET 9.0. Can you support .NET 8.0, too? `
It's too early for me to switch to .NET 9.0. Can you support .NET 8.0, too?
I second that. Not ready for .NET 9 yet.
that's interesting. I never updated any of the dependencies. I'll have a look @dradovic
I updated this sample project to use .NET 8 & v2.0.0-rc.1 of this library https://github.com/soundaranbu/Razor.Templating.Core/tree/main/examples/_RealWorldSamples/Invoice
But I don't find such exceptions. Could you please take a look at this sample & see what differs in your application? @dradovic
I just had a second look. It's all good and the feature works as expected! 🥇
Also, there's no regression according to our tests. So you can release your 2.0.0, AFAIC.
When I tried the last time, I was rushing too fast and instead of upgrading your package, I clicked on Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
(as one can see in the error that I pasted above) - thus the NuGet error. My apologies for that.
Thanks @dradovic, I'll release this next week
Stable version has been released now https://github.com/soundaranbu/Razor.Templating.Core/releases/tag/v2.0.0
Thanks to everyone!