An ASP.NET Core API can return a valid weather forecast response while the Angular page keeps showing an empty table. In an Angular zoneless template built with Angular 22, that combination points away from routing and JSON serialization and toward the component’s render-notification contract. A plain field can receive the HTTP result without telling Angular that a template consumer must be refreshed.
This guide diagnoses that exact failure shape in an ASP.NET Core with Angular solution, replaces the mutable field with signal-backed state, repairs the loading and error branches, and verifies both the browser path and the generated test setup. It is intentionally scoped to projects whose files match the checks below. It does not claim that every Visual Studio or Angular template version has the defect.
Table of Contents
Confirm the Angular zoneless template shape
Before changing code, separate three layers:
- The ASP.NET Core endpoint must return 200 with the expected JSON.
- Angular’s HttpClient subscription must receive that JSON.
- The component must notify Angular that template-visible state changed.
In browser developer tools, reload the forecast page and inspect the Network panel. If GET /weatherforecast fails, fix the backend, HTTPS proxy, or route first. The zoneless fix does not repair a 404, CORS failure, certificate problem, or incompatible response shape.
If the request succeeds, inspect the generated Angular component. The affected shape looks like this:
public forecasts: WeatherForecast[] = [];
this.http.get<WeatherForecast[]>('/weatherforecast').subscribe(
(result) => { this.forecasts = result; },
(error) => { console.error(error); }
);
Then inspect angular.json and package.json:
npm ls @angular/core @angular/cli @angular/build vitest jsdom zone.js
Angular documents that zoneless is the default in v21 and later. In that mode, Angular schedules change detection from explicit notifications such as updating a signal read by the template, AsyncPipe, a template listener, or ChangeDetectorRef.markForCheck. Assigning an ordinary field in an HttpClient callback is not one of those notifications.
This is a narrower problem than “HttpClient is broken.” The request has completed; the view did not receive a supported notification. Do not add ZoneJS back before proving that this is the boundary you crossed.
For the related case where Reactive Forms model updates leave template state stale, see Angular 22 Zoneless Reactive Forms: Why setValue() Can Leave the UI Stale—and How to Fix It. That article covers form notifications; the current guide stays focused on generated SPA scaffolding, HTTP-backed component state, and the test-runner mismatch.
Replace the mutable field with explicit UI state
Do not model loading with an empty array. Empty arrays are truthy, so an expression such as *ngIf=”forecasts” shows the table immediately and makes its loading branch unreachable. Also, a valid response can contain zero rows; loading and empty are different states.
Use signals for the data and error state:
import { Component, OnInit, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface WeatherForecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
@Component({
selector: 'app-root',
templateUrl: './app.html'
})
export class App implements OnInit {
private readonly http = inject(HttpClient);
readonly forecasts = signal<readonly WeatherForecast[] | undefined>(
undefined
);
readonly loadError = signal<string | undefined>(undefined);
ngOnInit(): void {
this.http.get<WeatherForecast[]>('/weatherforecast').subscribe({
next: (rows) => {
this.loadError.set(undefined);
this.forecasts.set(rows);
},
error: (error) => {
console.error('Weather forecast request failed', error);
this.loadError.set('The forecast could not be loaded.');
}
});
}
}
Reading forecasts() in the template registers the template as a signal consumer. Calling forecasts.set(rows) then schedules the update Angular needs in a zoneless application.
The readonly array type prevents callers from treating the returned array as an in-place mutation target. If later code must change one row, create a new array through update:
this.forecasts.update((rows) =>
rows?.map((row) =>
row.date === changed.date ? changed : row
)
);
A deep mutation such as forecasts()![0].summary = ‘Changed’ does not call set or update and should not be used as a notification mechanism.
Render loading, empty, error, and data separately
Use built-in control flow so the page expresses every state:
@if (loadError(); as message) {
<p role="alert">{{ message }}</p>
} @else if (forecasts(); as rows) {
@if (rows.length === 0) {
<p>No forecast data is available.</p>
} @else {
<table>
<thead>
<tr>
<th>Date</th>
<th>Temperature C</th>
<th>Temperature F</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@for (forecast of rows; track forecast.date) {
<tr>
<td>{{ forecast.date }}</td>
<td>{{ forecast.temperatureC }}</td>
<td>{{ forecast.temperatureF }}</td>
<td>{{ forecast.summary }}</td>
</tr>
}
</tbody>
</table>
}
} @else {
<p aria-live="polite">Loading...</p>
}
The track expression assumes date is unique in this sample. A production API should expose a stable identifier when rows can share a date or when sorting and replacement are important.
The error branch intentionally does not publish the raw HttpErrorResponse to the page. Log diagnostic detail through the application’s normal telemetry policy, but show a stable, non-sensitive message to the user. If a Retry button is added, put request creation in a method and clear both states before starting the next attempt.
Audit the generated test runner before adding packages
Some reported Visual Studio-generated snapshots combine Vitest dependencies with a Karma configuration that references packages absent from package.json. Do not solve that mismatch by blindly installing both runners.
Inspect the actual test target in angular.json. If it declares karmaConfig while package.json contains vitest and jsdom but not the Karma plugins, compare it with a clean Angular CLI workspace created with the same Angular version. For a Vitest-based workspace:
- Remove only the stale karmaConfig option.
- Delete karma.conf.js only after confirming no other script references it.
- Fix syntax errors in app.spec.ts before diagnosing runner behavior.
- Keep test-only packages in devDependencies and pin them through the project’s normal lockfile policy.
Search every reference first:
git grep -nE 'karmaConfig|karma\.conf|jest-editor-support|vitest'
The goal is one coherent runner, not a green command achieved by accumulating unrelated dependencies. If the repository deliberately standardized on Karma, follow that decision instead and install/configure it completely. The presence of a file name alone is not enough to choose a runner.
Add a test that proves the response reaches the DOM
A service-only test can prove that HttpClient returned data while missing the exact regression: the component received rows but the DOM stayed stale. Test the rendered result.
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting
} from '@angular/common/http/testing';
import { App } from './app';
describe('App', () => {
it('renders forecasts after the HTTP response', async () => {
await TestBed.configureTestingModule({
declarations: [App],
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
}).compileComponents();
const fixture = TestBed.createComponent(App);
const http = TestBed.inject(HttpTestingController);
fixture.detectChanges();
const request = http.expectOne('/weatherforecast');
expect(request.request.method).toBe('GET');
request.flush([
{
date: '2026-09-07',
temperatureC: 12,
temperatureF: 53,
summary: 'Cool'
}
]);
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Cool');
expect(fixture.nativeElement.textContent).not.toContain('Loading...');
http.verify();
});
});
Provider order matters: provideHttpClientTesting must follow provideHttpClient so the testing backend replaces the real backend. The assertion uses whenStable instead of forcing another detectChanges after flush; that verifies the signal notification can schedule the render rather than hiding a missing notification in the test.
Add two companion cases:
- Flush an empty array and assert the explicit empty-state message appears.
- Flush an HTTP error and assert the accessible error message appears while the table does not.
If the component is standalone, move App to imports instead of declarations. Keep the test aligned with the generated component shape.
Verify the combined ASP.NET Core and Angular path
Run checks from both project boundaries:
# Angular project
npm ci
npm test -- --watch=false
npm run build
# ASP.NET Core project
dotnet build
dotnet publish -c Release
Then start the HTTPS launch profile and verify:
- GET /weatherforecast returns 200 and an array.
- The page initially announces Loading.
- Rows appear after the response without a manual click or resize.
- An empty response shows the empty state.
- A failed response shows the error state without exposing server detail.
- The test process exits normally, and the production Angular build and dotnet publish both succeed.
Test the published output too. Development servers can hide integration mistakes through proxy behavior and hot reload. Confirm that the artifact produced by dotnet publish starts with the expected launch configuration and serves the Angular assets and API route in the deployment model you use.
This article has not executed the affected Visual Studio template in the current environment. The reproduction details and reported version boundary come from the linked upstream issue; the Angular notification behavior and the signal fix come from Angular’s documented zoneless contract. Re-run the matrix above on your exact Visual Studio, Angular, Node, and .NET versions before adopting or publishing a fixed-version claim.
Production risks and alternatives
Signals are the smallest fix when the component owns the data and template. AsyncPipe is also a valid notification path when the application prefers observable-backed view models. ChangeDetectorRef.markForCheck can be appropriate at a legacy boundary, but it is easier to forget on the next callback and leaves state representation implicit.
Do not use setTimeout, an artificial click, or a second HTTP request to wake the view. Those symptoms can appear to repair development mode while leaving the production contract broken. Do not call detectChanges from application code as a general replacement for reactive state.
For long-lived streams, clean up subscriptions with the project’s established lifecycle pattern, such as takeUntilDestroyed. HttpClient requests normally complete after one response, but retry, polling, or WebSocket layers change that assumption. Also decide how stale data behaves during refresh: clear the prior rows, keep them with a refreshing indicator, or swap only after the new response succeeds.
Finally, treat the template as scaffolding, not a maintained architecture boundary. Audit generated packages, test configuration, error handling, accessibility, and deployment scripts before building production features on top of it.
Angular zoneless template version status
The upstream report was opened on September 6, 2026 for a Visual Studio 18.9.12120.119 solution targeting .NET 10.0.400, with Angular packages resolving around 22.1.x. As of September 7, it remains open and is labeled External in the ASP.NET Core repository, which indicates that the template implementation is maintained outside that repository. No fixed Visual Studio version is claimed here.
Because the report is new and version-sensitive, check its current state before publication. If a template update ships, preserve the diagnostic and verification steps but change the version note to the confirmed fixed floor. Existing generated applications will not necessarily rewrite their component and test files when Visual Studio updates, so code-shape detection remains more reliable than IDE version alone.
References
- Microsoft Learn: Use Angular with ASP.NET Core
- Angular: Zoneless
- Angular: Signals
- Angular: Testing HTTP requests
- dotnet/aspnetcore issue #69078
Found this useful? Support more practical developer content.