Search code examples
c#unit-testingrazorengine

RazorEngine 3.4 throws System.ArgumentException: with cached @Layout and different Models


I have a problem with RazorEngine 3.4 caching. I have a few email templates with the same @Layout but different Models for each template. It works fine until I try to use the Cache I read that not use cache: "will result in both dreadful performances and memory leaks" from here.

So I turned it on. It was simple but lead to a problem: the _Layout.cshtml is also cached with the first model type, when I try to parse an another template with a different Model it will throw an exception: "System.ArgumentException: Object of type '....model1...' cannot be converted to type '...model2...'."

I wrote 2 unit tests into "IsolatedTemplateServiceTestFixture.cs" to show the problem. The first one passes, but the second one fails because the TemplateService.SetModelExplicit() function wants to set the template.Model property with a different Model type for the Layout.

private Mock<ITemplateResolver> _templateResolver;

    [Test]
    public void IsolatedTemplateService_CanParseTemplateWithLayout_WithOneSerializableModels_UseCache()
    {
        _templateResolver = new Mock<ITemplateResolver>();
        var config = new TemplateServiceConfiguration()
        {
            Resolver = _templateResolver.Object
        };

        using (var service = new TemplateService(config))
        {
            _templateResolver.Setup(i => i.Resolve("test")).Returns("<html>@RenderBody()</html>");

            const string template = @"@{Layout=""test"";}<h1>Hello @Model.Item1</h1>";
            const string expected = "<html><h1>Hello World</h1></html>";

            var model = new Tuple<string>("World");
            string result = service.Parse(template, model, null, "C1");
            string result2 = service.Parse(template, model, null, "C1");

            Assert.That(result == expected, "Result does not match expected: " + result);
            Assert.That(result2 == expected, "Result does not match expected: " + result2);
        }
    }

    [Test]
    public void IsolatedTemplateService_CanParseTemplateWithLayout_WithDifferentSerializableModels_UseCache()
    {
        _templateResolver = new Mock<ITemplateResolver>();
        var config = new TemplateServiceConfiguration()
        {
            Resolver = _templateResolver.Object
        };

        using (var service = new TemplateService(config))
        {
            _templateResolver.Setup(i => i.Resolve("test")).Returns("<html>@RenderBody()</html>");

            const string template = @"@{Layout=""test"";}<h1>Hello @Model.Item1</h1>";
            const string expected = "<html><h1>Hello World</h1></html>";

            var model = new Tuple<string>("World");
            string result = service.Parse(template, model, null, "C1");
            string result2 = service.Parse(template, model, null, "C1");

            const string template2 = @"@{Layout=""test"";}<h1>Hello2 @Model.Item1</h1>";
            const string expected2 = "<html><h1>Hello2 123</h1></html>";
            var model2 = new Tuple<int>(123);

            string result3 = service.Parse(template2, model2, null, "C2");

            Assert.That(result == expected, "Result does not match expected: " + result);
            Assert.That(result2 == expected, "Result does not match expected: " + result2);

            Assert.That(result3 == expected2, "Result does not match expected: " + result3);
        }
    }

My question is: anybody has the same problem? What would a "nice" way to workaround it until it will be fixed (if at all it happens)?

Update:

With the latest version (currently is v.3.10) both tests passed. So the problem was fixed.


Solution

  • As it is in RazorEngine, the layout has a fixed type, even if you don't declare a model in it. The first time the layout gets compiled by compiling a template, the template's model type becomes the layout's type as well. As you noticed, that will clash when you try to compile another template with a different type.

    You can work around that by declaring the model type of the layout dynamic, i.e. @model dynamic

    That should do the trick. The actual templates don't need to be altered.