Razor.Templating.Core icon indicating copy to clipboard operation
Razor.Templating.Core copied to clipboard

New Feature: Check if template exists

Open pbolduc opened this issue 2 years ago • 10 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);

pbolduc avatar Sep 16 '22 15:09 pbolduc

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?

soundaranbu avatar Sep 16 '22 17:09 soundaranbu

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.

pbolduc avatar Sep 16 '22 21:09 pbolduc

Ok, then we need to be more explicit on the method name. TryRenderIfViewExistsAsync() would be more appropriate. What do you think?

soundaranbu avatar Sep 18 '22 05:09 soundaranbu

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.

pbolduc avatar Sep 19 '22 18:09 pbolduc

That's a good idea! I'll think through it & get back to you.

soundaranbu avatar Sep 22 '22 13:09 soundaranbu

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.

dradovic avatar Jul 26 '23 10:07 dradovic

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.

soundaranbu avatar Jul 26 '23 10:07 soundaranbu

Unrelated comment - thanks for a great library! 🥇

shapeh avatar Oct 12 '23 17:10 shapeh

I guess this isn't release yet, or is it?

dradovic avatar Feb 28 '24 14:02 dradovic

Hey @dradovic, this is not released yet. Required changes are already done but needs some minor cleanup. I'll probably do it this week.

soundaranbu avatar Feb 28 '24 14:02 soundaranbu

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

soundaranbu avatar Mar 04 '24 22:03 soundaranbu

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? `

dradovic avatar Mar 06 '24 12:03 dradovic

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.

shapeh avatar Mar 06 '24 13:03 shapeh

that's interesting. I never updated any of the dependencies. I'll have a look @dradovic

soundaranbu avatar Mar 06 '24 14:03 soundaranbu

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

soundaranbu avatar Mar 07 '24 10:03 soundaranbu

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.

dradovic avatar Mar 08 '24 09:03 dradovic

Thanks @dradovic, I'll release this next week

soundaranbu avatar Mar 09 '24 07:03 soundaranbu

Stable version has been released now https://github.com/soundaranbu/Razor.Templating.Core/releases/tag/v2.0.0

Thanks to everyone!

soundaranbu avatar Apr 18 '24 08:04 soundaranbu