Statiq.Framework
Statiq.Framework copied to clipboard
RenderRazor().WithModel raise error with strongly-typed ViewModel
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
- I have marked the state by this tag
- 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
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):
First step is to replicate in a failing test...
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:
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.
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.
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!
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.
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)
.
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.
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.