-
Notifications
You must be signed in to change notification settings - Fork 0
Fix PersistentState property visibility and favicon 302 redirect with correct pipeline ordering #522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix PersistentState property visibility and favicon 302 redirect with correct pipeline ordering #522
Conversation
Co-authored-by: dwarwick <15970276+dwarwick@users.noreply.github.com>
Introduced `[JsonConverter]` attributes to `AnswerViewModel` and `QuestionViewModel` classes to specify the use of custom JSON converters (`AnswerViewModelConverter` and `QuestionViewModelConverter`, respectively). These changes enable custom serialization and deserialization logic for these abstract ViewModel classes.
|
@copilot my favicon always returns a 302, which results in a not found redirect |
Co-authored-by: dwarwick <15970276+dwarwick@users.noreply.github.com>
Fixed in commit c6a5457. The issue was that browsers automatically request Solution:
The favicon will now load correctly without any 302 redirects. |
Co-authored-by: dwarwick <15970276+dwarwick@users.noreply.github.com>
|
that didn't help. Instead of writing special middleware for the favicon, what would be the more correct solution? |
|
@copilot , |
…o file Co-authored-by: dwarwick <15970276+dwarwick@users.noreply.github.com>
... Fixed in commit ff59d7c. The more correct solution: Add The problem: Your pipeline had What changed:
Regarding pipeline order: Yes, your pipeline needed reordering. The standard ASP.NET Core pattern is:
This is the recommended Microsoft approach - static files should be served early, bypassing auth and error handling. |
The `@import` statement for `open-iconic-bootstrap.min.css` in `app-light.css` was commented out, effectively disabling the Open Iconic font CSS. Additionally, the `favicon.ico` file was updated, with the binary diff indicating a change to the favicon image.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes two distinct issues: a component state persistence error and a favicon 302 redirect problem. The Survey property visibility was causing InvalidOperationException during Blazor's component state persistence because the framework requires public access for reflection-based serialization. The favicon redirect issue was resolved by placing UseStaticFiles() earlier in the middleware pipeline, ensuring static assets are served before 404 handling logic.
- Changed
Surveyproperty fromprotectedtopublicto satisfy[PersistentState]attribute requirements - Added
UseStaticFiles()middleware beforeUseStatusCodePages()to serve static files early in the request pipeline - Added
[JsonConverter]attributes toQuestionViewModelandAnswerViewModelto support polymorphic JSON serialization for persistent state
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| JwtIdentity.Client/Pages/Survey/Survey.razor.cs | Changed Survey property visibility from protected to public to enable PersistentState serialization |
| JwtIdentity/Program.cs | Added UseStaticFiles() middleware early in pipeline (after UseHttpsRedirection, before UseStatusCodePages) |
| JwtIdentity/wwwroot/favicon.ico | Added favicon.ico file to serve as the site icon |
| JwtIdentity/Components/App.razor | Added explicit favicon link reference |
| JwtIdentity.Common/ViewModels/QuestionViewModel.cs | Added JsonConverter attribute to enable polymorphic serialization of abstract question types |
| JwtIdentity.Common/ViewModels/AnswerViewModel.cs | Added JsonConverter attribute to enable polymorphic serialization of abstract answer types |
| JwtIdentity/wwwroot/css/app-light.css | Removed unused open-iconic import statement |
Comments suppressed due to low confidence (14)
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:388
- This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
foreach (var question in Survey.Questions)
{
if (question.Id == answer.QuestionId)
{
for (int i = 0; i < question.Answers.Count; i++)
{
if (question.Answers[i].Id == answer.Id)
{
question.Answers[i] = !Preview ? response : answer;
}
}
}
}
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:755
- This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
foreach (var question in groupQuestions)
{
if (question.QuestionType == QuestionType.SelectAllThatApply)
{
var saQuestion = question as SelectAllThatApplyQuestionViewModel;
var answer = question.Answers.FirstOrDefault() as SelectAllThatApplyAnswerViewModel;
if (answer != null && saQuestion != null)
{
// Ensure SelectedOptions list is properly sized
if (answer.SelectedOptions == null)
{
answer.SelectedOptions = new List<bool>();
}
while (answer.SelectedOptions.Count < saQuestion.Options.Count)
{
answer.SelectedOptions.Add(false);
}
// Populate based on saved SelectedOptionIds
if (!string.IsNullOrWhiteSpace(answer.SelectedOptionIds))
{
var selectedOptionIds = answer.SelectedOptionIds.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(int.Parse).ToList();
for (int i = 0; i < saQuestion.Options.Count && i < answer.SelectedOptions.Count; i++)
{
answer.SelectedOptions[i] = selectedOptionIds.Contains(saQuestion.Options[i].Id);
}
}
}
}
}
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:807
- This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
foreach (var question in lastQuestions)
{
if (question.QuestionType == QuestionType.SelectAllThatApply)
{
var saQuestion = question as SelectAllThatApplyQuestionViewModel;
var answer = question.Answers.FirstOrDefault() as SelectAllThatApplyAnswerViewModel;
if (answer != null && saQuestion != null)
{
// Ensure SelectedOptions list is properly sized
if (answer.SelectedOptions == null)
{
answer.SelectedOptions = new List<bool>();
}
while (answer.SelectedOptions.Count < saQuestion.Options.Count)
{
answer.SelectedOptions.Add(false);
}
// Populate based on saved SelectedOptionIds
if (!string.IsNullOrWhiteSpace(answer.SelectedOptionIds))
{
var selectedOptionIds = answer.SelectedOptionIds.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(int.Parse).ToList();
for (int i = 0; i < saQuestion.Options.Count && i < answer.SelectedOptions.Count; i++)
{
answer.SelectedOptions[i] = selectedOptionIds.Contains(saQuestion.Options[i].Id);
}
}
}
}
}
JwtIdentity/Components/App.razor:79
- String concatenation in loop: use 'StringBuilder'.
metaHtml += $"<meta {attribute}='{tag.Key}' content='{tag.Value}'>";
JwtIdentity/Components/App.razor:102
- String concatenation in loop: use 'StringBuilder'.
metaHtml += $"<meta {attribute}='{tag.Key}' content='{tag.Value}'>";
JwtIdentity.Common/ViewModels/AnswerViewModel.cs:22
- Testing whether 'this' is an instance of TrueFalseAnswerViewModel in AnswerViewModel introduces a dependency cycle between the two types.
=> this is TrueFalseAnswerViewModel tf ? tf.Value : null;
JwtIdentity.Common/ViewModels/AnswerViewModel.cs:26
- Testing whether 'this' is an instance of TextAnswerViewModel in AnswerViewModel introduces a dependency cycle between the two types.
=> this is TextAnswerViewModel ta ? ta.Text : null;
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:545
- The expression 'A == false' can be simplified to '!A'.
if (question.IsRequired == false)
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:976
- The expression 'A == true' can be simplified to 'A'.
if (tfAnswer.Value.Value == true && tfQuestion.BranchToGroupIdOnTrue.HasValue)
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:980
- The expression 'A == false' can be simplified to '!A'.
else if (tfAnswer.Value.Value == false && tfQuestion.BranchToGroupIdOnFalse.HasValue)
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:1041
- The expression 'A == true' can be simplified to 'A'.
if (tfAnswer.Value.Value == true && tfQuestion.BranchToGroupIdOnTrue.HasValue && !_visitedGroups.Contains(tfQuestion.BranchToGroupIdOnTrue.Value))
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:1045
- The expression 'A == false' can be simplified to '!A'.
else if (tfAnswer.Value.Value == false && tfQuestion.BranchToGroupIdOnFalse.HasValue && !_visitedGroups.Contains(tfQuestion.BranchToGroupIdOnFalse.Value))
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:108
- Field '_visitedGroups' can be 'readonly'.
private HashSet<int> _visitedGroups = new();
JwtIdentity.Client/Pages/Survey/Survey.razor.cs:76
- Both branches of this 'if' statement return - consider using '?' to express intent better.
if (DemoType == "branching")
{
// Branching demo - only show for branching surveys
return isBranchingSurvey;
}
else
{
// Linear or other demo type - only show for non-branching surveys
return !isBranchingSurvey;
}


Fixes two issues: the
Surveyproperty visibility causingInvalidOperationExceptionduring component state persistence, and the favicon 302 redirect issue by correcting the middleware pipeline ordering.Changes Made
1. Survey Property Visibility Fix
Made
Surveypropertypublicto satisfy[PersistentState]attribute requirements:The Blazor framework uses reflection to access
[PersistentState]properties and requires public visibility.2. Favicon 302 Redirect Fix - Pipeline Ordering Correction
Root Cause: The middleware pipeline had
MapStaticAssets()at the end, butUseStatusCodePages()ran before static files were checked. This caused/favicon.icorequests to hit 404 logic before the file could be served, resulting in 302 redirects to/not-found.Solution - Standard ASP.NET Core approach:
UseStaticFiles()early in the pipeline (afterUseHttpsRedirection(), beforeUseStatusCodePages()) inProgram.csfavicon.icofile inwwwrootdirectoryApp.razorwith explicit favicon link:<link rel="icon" type="image/x-icon" href="favicon.ico" />Correct Pipeline Order:
This follows Microsoft's recommended ASP.NET Core middleware ordering where static files are served early in the pipeline, bypassing authentication and error handling logic.
Testing
The changes use standard ASP.NET Core patterns, fixing both the component state persistence issue and the favicon loading problem without custom middleware workarounds.
Original prompt
This section details on the original issue you should resolve
<issue_title>Fix Error During Demo</issue_title>
<issue_description>During a Branching Demo, I get the following error in the dev console. The URL is https://localhost:5001/survey/676b5f29-7e85-414b-b22a-1c117860fe01?Preview=true&DemoStep=0&DemoType=branching
System.InvalidOperationException: A public property 'Survey' on component type 'JwtIdentity.Client.Pages.Survey.Survey' with a public getter wasn't found.
at Microsoft.AspNetCore.Components.Infrastructure.PersistentValueProviderComponentSubscription.PropertyGetterFactory(ValueTuple
2 key) at System.Collections.Concurrent.ConcurrentDictionary2.GetOrAdd(TKey key, Func`2 valueFactory)at Microsoft.AspNetCore.Components.Infrastructure.PersistentValueProviderComponentSubscription..ctor(PersistentComponentState state, ComponentState subscriber, CascadingParameterInfo parameterInfo, IServiceProvider serviceProvider, ILogger logger)
at Microsoft.AspNetCore.Components.Infrastructure.PersistentStateValueProvider.Subscribe(ComponentState subscriber, CascadingParameterInfo& parameterInfo)
at Microsoft.AspNetCore.Components.Rendering.ComponentState.AddCascadingParameterSubscriptions()
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.g__Execute|58_0()
at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.WaitForResultReady(Boolean waitForQuiescence, PrerenderedComponentHtmlContent result)
at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.RenderEndpointComponent(HttpContext httpContext, Type rootComponentType, ParameterView parameters, Boolean waitForQuiescence)
at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<b__10_0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
...
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.