New Feature: Check if template exists
pbolduc opened this issue · 18 comments
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! 🥇