Statiq.Framework icon indicating copy to clipboard operation
Statiq.Framework copied to clipboard

RenderRazor().WithModel raise error with strongly-typed ViewModel

Open Simply007 opened this issue 3 years ago • 9 comments

Description

When using a Pipeline to generate a page using Razor view with a strongly typed view model - LandingPage - I am getting an error:

Error:

Content/PostProcess » RenderContentPostProcessTemplates » ExecuteIf » ExecuteIf » RenderRazor » The model item passed into the ViewDataDictionary is of type 'Statiq.Common.Document', but this ViewDataDictionary instance requires a model item of type 'Jamstack.On.Dotnet.Models.LandingPage

Version of Statiq.Razor: 1.0.0-beta.27 I am using Kontent.Static module to pull data from Kentico Kontent headless CMS.

Pipeline

public class Index : Pipeline
    {
        public Index(IDeliveryClient client)
        {
            InputModules = new ModuleList
            {
                // Load home page
                new Kontent<LandingPage>(client)
                    .WithQuery(new EqualsFilter("system.codename", "home_page")), 
                // Set the output path for each article
                new SetDestination(Config.FromDocument((doc, ctx)
                  => new NormalizedPath($"index.html"))),
            };

            ProcessModules = new ModuleList
            {
                new MergeContent(new ReadFiles("Index.cshtml")),
                new RenderRazor()
                    .WithModel(Config.FromDocument((document, context) => 
                        document.AsKontent<LandingPage>()))  // << PROBLEMATIC PART
            };

            OutputModules = new ModuleList {
                new WriteFiles()
            };
        }
    }

document.AsKontent<LandingPage>() returns LandingPage, so there should be no problem in the Razor template rendering.

Template

@model Jamstack.On.Dotnet.Models.LandingPage

<h1>@Model.Headline</h1>
<p>Hello from my Statiq page.</p>

@daveaglick - I have invited you to the repository with the code, so that you could check the code and the log,

  • Code is here: https://github.com/Kentico/jamstackon.net/blob/main/Pipelines/Index.cs
  • Log is here: https://github.com/Kentico/jamstackon.net/runs/1315248074?check_suite_focus=true#step:6:689
[DBUG] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » ExecuteIf » Starting module execution... (0 input document(s))
[ERRO] Content/PostProcess » RenderContentPostProcessTemplates » ExecuteIf » ExecuteIf » RenderRazor » The model item passed into the ViewDataDictionary is of type 'Statiq.Common.Document', but this ViewDataDictionary instance requires a model item of type 'Jamstack.On.Dotnet.Models.LandingPage'.
[DBUG] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » ExecuteIf » Finished module execution (0 output document(s), 4 ms)
[DBUG] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » ExecuteIf » Starting module execution... (0 input document(s))
[DBUG] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » ExecuteIf » Finished module execution (0 output document(s), 0 ms)
[DBUG] Archives/PostProcess » ExecuteSwitch » RenderContentPostProcessTemplates » Finished module execution (0 output document(s), 6 ms)
[DBUG] Archives/PostProcess » ExecuteSwitch » Finished module execution (0 output document(s), 8 ms)
[INFO] <- Archives/PostProcess » Finished Archives PostProcess phase execution (0 output document(s), 8 ms)
[INFO] -> Archives/Output » Starting Archives Output phase execution... (0 input document(s), 2 module(s))
[DBUG] Archives/Output » FilterDocuments » Starting module execution... (0 input document(s))
[DBUG] Archives/Output » FilterDocuments » Finished module execution (0 output document(s), 0 ms)
[DBUG] Archives/Output » WriteFiles » Starting module execution... (0 input document(s))
[DBUG] Archives/Output » WriteFiles » Finished module execution (0 output document(s), 0 ms)
[INFO] <- Archives/Output » Finished Archives Output phase execution (0 output document(s), 0 ms)
[DBUG] Sitemap/PostProcess » ExecuteIf » GenerateSitemap » Finished module execution (1 output document(s), 24 ms)
[DBUG] Sitemap/PostProcess » ExecuteIf » Finished module execution (1 output document(s), 25 ms)
[INFO] <- Sitemap/PostProcess » Finished Sitemap PostProcess phase execution (1 output document(s), 25 ms)
[INFO] -> Sitemap/Output » Starting Sitemap Output phase execution... (1 input document(s), 1 module(s))
[DBUG] Sitemap/Output » WriteFiles » Starting module execution... (1 input document(s))
[DBUG] Sitemap/Output » WriteFiles » Wrote file /home/runner/work/jamstackon.net/jamstackon.net/output/sitemap.xml from 
[DBUG] Sitemap/Output » WriteFiles » Finished module execution (1 output document(s), 0 ms)
[INFO] <- Sitemap/Output » Finished Sitemap Output phase execution (1 output document(s), 0 ms)
[DBUG] Exception while executing pipeline Content/PostProcess: System.InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'Statiq.Common.Document', but this ViewDataDictionary instance requires a model item of type 'Jamstack.On.Dotnet.Models.LandingPage'.
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.EnsureCompatible(Object value)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary..ctor(ViewDataDictionary source, Object model, Type declaredModelType)
   at lambda_method(Closure , ViewDataDictionary )
   at Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator.CreateViewDataDictionary(ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator.Activate(Object page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorPageActivator.Activate(IRazorPage page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
   at Statiq.Razor.RazorCompiler.RenderPageAsync(RenderRequest request)
   at Statiq.Razor.RazorService.RenderAsync(RenderRequest request)
   at Statiq.Razor.RenderRazor.<>c__DisplayClass14_0.<<ExecuteContextAsync>g__RenderDocumentAsync|1>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Statiq.Common.ParallelAsyncExtensions.ParallelSelectAsync[TSource,TResult](IEnumerable`1 items, Func`2 asyncSelector, CancellationToken cancellationToken)
   at Statiq.Razor.RenderRazor.ExecuteContextAsync(IExecutionContext context)
   at Statiq.Common.Module.ExecuteAsync(IExecutionContext context)
   at Statiq.Common.Module.ExecuteAsync(IExecutionContext context)
   at Statiq.Core.Engine.ExecuteModulesAsync(ExecutionContextData contextData, IExecutionContext parent, IEnumerable`1 modules, ImmutableArray`1 inputs, ILogger logger)
[DBUG] Content/Output » Skipping pipeline due to dependency error
[DBUG] AnalyzeContent/Input » Skipping pipeline due to dependency error
[DBUG] AnalyzeContent/Process » Skipping pipeline due to dependency error
[DBUG] AnalyzeContent/PostProcess » Skipping pipeline due to dependency error
[DBUG] AnalyzeContent/Output » Skipping pipeline due to dependency error

Simply007 avatar Oct 27 '20 14:10 Simply007

Thanks for the detailed bug report! I can reproduce locally and it certainly looks like there's a mismatch between what the page is expecting (a LandingPage) and the model in the view data (which is definitely a Statiq document):

image

First step is to replicate in a failing test...

daveaglick avatar Oct 30 '20 20:10 daveaglick

Okay, tracked this one down. The problem is that Statiq Web is being used in combination with custom pipelines. While the Index pipeline defined explicitly does pass in a custom view model, Statiq Web also executes a whole bunch of other convention-based pipelines, one of which also attempts to render Razor files. When that pipeline runs, it doesn't have the same .WithModel() call that the RenderRazor module in your Index pipeline has, so it gets to the Razor page and it's @model directive and there's a mismatch because that RenderRazor sets test model to the current document (the default behavior).

Even if this problem hadn't exhibited in this way, it still would have caused problems having two pipelines process the same files. There's a few different things you could do - let me know which is preferred and I'll walk you through it:

  • Turn off the built-in Statiq Web pipelines. The bootstrapper call to .CreateWeb(args) looks like this:
public static Bootstrapper AddWeb(this Bootstrapper boostrapper) =>
  boostrapper
    .AddPipelines(typeof(BootstrapperFactoryExtensions).Assembly)
    .AddHostingCommands()
    .AddWebServices()
    .AddInputPaths()
    .AddExcludedPaths()
    .SetOutputPath()
    .AddThemePaths()
    .AddDefaultWebSettings()
    .AddWebAnalyzers()
    .ConfigureEngine(e => e.LogAndCheckVersion(typeof(BootstrapperExtensions).Assembly, "Statiq Web", WebKeys.MinimumStatiqWebVersion));

You could each of those directly and omit the .AddPipelines() call. That's a maintenance burden though because if the set of stuff CreateWeb() calls expands you'll have to take note and also expand it. You could also use the ConfigureEngine() bootstrapper extension to iterate the pipelines once they're all added and remove any you don't want.

Turning off or removing the Statiq Web pipelines isn't great because they do a lot for you, so if you want to keep using them you could also...

  • Change the Razor template

The rendering calls in Statiq Web use something called templates that keeps them modular and extensible. Basically a template is a media type key with a module definition for those types of documents. The existing one looks like this: image

You could customize that like this:

.ConfigureTemplates(templates =>
{
    ((RenderRazor)templates[MediaTypes.Razor].Module)
        .WithModel(Config.FromDocument((document, context) => 
            document.AsKontent<LandingPage>()));
})

Of course you'd probably want to change the module based on the document path or something, but that shows you how to adjust templates.

It looks like you're doing other stuff in that Index pipeline though, so another option is...

  • Make certain pages avoid the built-in pipelines

You can exclude a document from processing by setting Excluded: true in it's front matter (which means you'd also need to process that front matter out of the file with the ExtractFrontMatter module, unless you used a sidecar file to set the Excluded metadata). That will exclude the document from the built-in pipelines but you can still use it in your own pipelines. This probably seems the most like what you want to do.

Did all that makes sense? Happy to clarify further or help work though how to set any of these strategies up.

daveaglick avatar Oct 30 '20 21:10 daveaglick

Thought of another approach that might work even better. If you name the index file _Index.cshtml with an underscore the built-in Statiq Web pipelines will ignore it. Then use a SetDestination module to change the destination to Index.html after your RenderRazor and you should be good to go.

daveaglick avatar Oct 31 '20 12:10 daveaglick

Thanks a lot @daveaglick!

For now, I will stick with the _Index.cshtml solution. I will consider using sidecar file to exclude from processing if I really need to have Index.cshtm as a file name, but it is not an issue for me now. THX!

Simply007 avatar Nov 02 '20 09:11 Simply007

Internal link: https://github.com/Kentico/statiq-kontent-collaboration/issues/16

After discussion with @daveaglick, I am opening up the issue with a more general solution proposal:

  • solutions proposal 1: basically mechanism to ignore some templates as a patter -> instead of the necessity of a sidecar file
  • solutions proposal 2: the templates do not need to be in the input folder, we could have them in separate solder as i.e. kontent that would not be handled default preferably

Let's switch this issue from the bug to enhancement request and define the functionality.

Simply007 avatar Nov 04 '20 15:11 Simply007

I read through the issue and I'm still wondering why my or @alanta's custom pipelines work fine...

petrsvihlik avatar Nov 05 '20 09:11 petrsvihlik

I read through the issue and I'm still wondering why my or @alanta's custom pipelines work fine...

It might be because I am using .CreateWeb(args) in Bootstrapper, but you and alanta is using .CreateDefault(args).

Simply007 avatar Nov 05 '20 09:11 Simply007

That's exactly right. Remember, this isn't so much a case of it not working, as it working too many times.

The .CreateDefault(args) creates a boostrapper and populates it with the default set of Statiq Framework functionality (like reading pipelines via reflection, getting settings from environment variables, etc.). Statiq Framework doesn't include any built-in pipelines though so the custom pipeline is the only one trying to read the index files.

On the other hand, .CreateWeb(args) calls .CreateDefault(args) internally, but also adds additional functionality from Statiq Web including the built-in Statiq Web pipelines. One of those reads .cshtml files so it picks up the index file which is expecting a custom model, but because that built-in Statiq Web pipelines doesn't know anything about setting the custom model in its own RenderRazor module, the custom model never gets set and the Razor engine crashes.

daveaglick avatar Nov 05 '20 12:11 daveaglick

Just for a fill context:

In the end, I have decided to place the "top-level" templates - used for pages themselves, where I can specify the template manually store in _partials/TEMPLATE.cshtml (i.e. /input/_partials/LandingPage.cshtml) and use display templates with sidecar file, because:

Shared/DisplayTemplates does require the name same as the Model provided in order to use automatic matching to models when a Structure rich text rendering is used.

FYI: I have submitted a separate issue for Pipeline specific ViewModel discovery #148 - it is a bit related.

Simply007 avatar Nov 11 '20 15:11 Simply007