Angular with .NET
Key Points
- Angular 18+ is opinionated: TypeScript-first, RxJS reactive, hierarchical DI, batteries-included router/forms/HTTP. Modern Angular = standalone components + signals + control flow (
@if/@for/@switch) + deferred views. - State: local UI state in component signals; cross-cutting state via NgRx (store/actions/reducers/effects/selectors) or the modern NgRx Signal Store.
- Integration with ASP.NET Core: Angular SPA + Web API behind same origin (BFF) or CORS-enabled cross-origin. OIDC via Auth0 / Entra ID; tokens via interceptors.
- Deploy:
ng buildtodist/, copy towwwroot/(single host) or Azure Static Web Apps with linked .NET API. Monorepo or two repos — both work. - When to pick Angular over Blazor/React: large enterprise teams, strict architecture, heavy forms/data-grid apps, existing Angular muscle. Pick Blazor if .NET-only; React for nimble SPAs and broader hiring pool.
- 💡 Senior .NET devs are productive fast in Angular: DI mental model maps cleanly, RxJS is the new IObservable, services are scoped just like ASP.NET Core's container.
Concepts (deep dive)
Angular at 30,000 feet (for the .NET dev)
| .NET concept | Angular equivalent |
|---|---|
IServiceCollection / DI | @Injectable({providedIn: 'root'}) + hierarchical injectors |
| Razor / Blazor component | Angular component (@Component) |
IObservable<T> (Rx.NET) | Observable<T> (RxJS) |
| Minimal API endpoint | HttpClient.get<T>() call |
IConfiguration | environment.ts files + DI tokens |
[Authorize] policy | CanActivateFn route guard |
| Middleware pipeline | HttpInterceptor chain |
IOptions<T> | InjectionToken<T> + provide |
Standalone components (Angular 18+)
NgModules are out. Components import what they need.
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<button (click)="inc()">+</button>
<span>{{ count() }}</span>
<span>doubled: {{ doubled() }}</span>
`
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
inc() { this.count.update(v => v + 1); }
}
bootstrapApplication(AppComponent, { providers: [...] }) replaces platformBrowserDynamic().bootstrapModule(AppModule).
Signals — the new reactivity primitive
Three flavours: signal(), computed(), effect().
const name = signal('Ada');
const greeting = computed(() => `Hello, ${name()}`);
effect(() => console.log(greeting())); // logs on every change
name.set('Grace'); // greeting + effect rerun
Replaces a lot of RxJS for view-local state. Change detection via signals is fine-grained — only the components reading a changed signal re-render.
Control flow (@if / @for / @switch)
@if (user(); as u) {
<p>Hi {{ u.name }}</p>
} @else {
<p>Sign in</p>
}
@for (item of items(); track item.id) {
<li>{{ item.name }}</li>
} @empty {
<p>No items.</p>
}
@switch (status()) {
@case ('loading') { <spinner/> }
@case ('error') { <error-banner/> }
@default { <results/> }
}
Compile-time checked, faster than *ngIf/*ngFor structural directives. Always supply track for @for (perf-critical for large lists).
Deferred views
Lazy-load template chunks based on triggers.
@defer (on viewport) {
<heavy-chart [data]="data()"/>
} @placeholder { <skeleton/> }
@loading { <spinner/> }
@error { <p>Failed.</p> }
Triggers: on idle, on viewport, on interaction, on hover, on timer(2s), when expr().
RxJS in 60 seconds
import { Subject, BehaviorSubject, fromEvent } from 'rxjs';
import { debounceTime, switchMap, map, takeUntil } from 'rxjs/operators';
Subject<T>: cold sink → multicast hot stream. No initial value.BehaviorSubject<T>: likeSubjectbut holds current value; new subscribers get it instantly.- Cold observable: each subscription gets its own producer (e.g.
HttpClient.get). - Hot observable: shared producer (mouse events, subjects).
takeUntil(destroy$): canonical unsubscribe pattern (or usetakeUntilDestroyed()in v16+).
@Component({...})
export class SearchComponent {
private destroyRef = inject(DestroyRef);
query = new Subject<string>();
results$ = this.query.pipe(
debounceTime(250),
switchMap(q => this.api.search(q)),
takeUntilDestroyed(this.destroyRef)
);
}
In templates use | async — no manual subscribe needed.
State: NgRx classic vs Signal Store
NgRx classic (Redux pattern)
export const loadUsers = createAction('[Users] Load');
export const loadUsersSuccess = createAction(
'[Users] Load Success',
props<{ users: User[] }>()
);
export const usersReducer = createReducer(
initialState,
on(loadUsersSuccess, (s, { users }) => ({ ...s, users }))
);
@Injectable()
export class UsersEffects {
load$ = createEffect(() => this.actions$.pipe(
ofType(loadUsers),
switchMap(() => this.api.getUsers().pipe(
map(users => loadUsersSuccess({ users }))
))
));
constructor(private actions$: Actions, private api: ApiService) {}
}
export const selectUsers = createFeatureSelector<UsersState>('users');
NgRx Signal Store (modern)
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState({ users: [] as User[], loading: false }),
withMethods((store, api = inject(ApiService)) => ({
async load() {
patchState(store, { loading: true });
const users = await firstValueFrom(api.getUsers());
patchState(store, { users, loading: false });
}
})),
withComputed(({ users }) => ({
count: computed(() => users().length)
}))
);
Less ceremony. Use for new code unless you already have NgRx classic.
Routing & guards
export const routes: Routes = [
{ path: 'home', component: HomeComponent },
{
path: 'admin',
canActivate: [authGuard],
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
},
{ path: '**', redirectTo: 'home' }
];
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};
loadComponent = lazy-loaded route. loadChildren = lazy feature with sub-routes.
Hierarchical DI vs .NET DI
In .NET Core, services live in one container (root + scoped). In Angular, every component injector is a child of its parent's injector:
RootInjector (providedIn: 'root')
↓
RouteInjector (providers in lazy route)
↓
ComponentInjector (providers: [] in component decorator)
Provide a service at the component level → fresh instance per component instance. Provide at root → singleton. This is more flexible than .NET's three lifetimes (singleton/scoped/transient) but easier to misuse.
Forms: template-driven vs reactive
Template-driven (simple)
<input [(ngModel)]="name" required minlength="2" #nameRef="ngModel">
@if (nameRef.invalid && nameRef.touched) { <p>Bad name.</p> }
Reactive (preferred for non-trivial forms)
form = inject(FormBuilder).group({
email: ['', [Validators.required, Validators.email]],
age: [0, [Validators.min(18)]]
});
submit() {
if (this.form.invalid) return;
this.api.save(this.form.getRawValue()).subscribe();
}
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="email">
<input formControlName="age" type="number">
</form>
Typed reactive forms (Angular 14+): FormGroup<{email: FormControl<string>}> — strongly typed end to end.
HttpClient + interceptors
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
}));
};
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([authInterceptor]))]
});
Interceptors handle: auth headers, retry, telemetry, error normalization, loading-spinner counters.
Angular SPA + ASP.NET Core API
Two integration shapes
Same-origin (BFF) Cross-origin (CORS)
───────────────── ───────────────────
browser browser
│ │
▼ ┌────────┴────────┐
ASP.NET host ng dev server .NET API
├─ / (Angular) :4200 :5000
└─ /api/* (controllers)
Same-origin = no CORS, cookies just work, BFF can hold tokens server-side. Cross-origin = SPA on CDN, API elsewhere — easier to scale, but you wear the CORS + token-storage cost.
CORS in ASP.NET Core
builder.Services.AddCors(o => o.AddPolicy("spa", p => p
.WithOrigins("https://app.contoso.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()));
app.UseCors("spa");
OIDC auth (Auth0 / Entra ID)
angular-auth-oidc-client or @auth0/auth0-angular. Login redirect → ID/access token → interceptor attaches access token to API calls.
For sensitive apps prefer the BFF pattern (see Frontend: React with .NET): tokens live in an HttpOnly cookie on the .NET host; SPA never sees a JWT. ⚠️ This is the Microsoft-recommended posture for 2024+.
Monorepo file structure
/src
/Api ← ASP.NET Core (Program.cs, Controllers/)
/Web
angular.json
package.json
/src
/app
/features/users ← feature folder = component + service + store
/features/orders
/core ← interceptors, guards, base services
/shared ← pipes, dumb components
/tests
/.editorconfig
/Directory.Build.props
*.sln
Build pipeline: npm ci && ng build --configuration production → output to Api/wwwroot/ → dotnet publish.
Static files in ASP.NET Core
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html"); // SPA deep-link fallback
Deployment options
| Option | Notes |
|---|---|
| Single .NET host (wwwroot) | Simplest. One process, one URL. Good for intranet. |
| Azure App Service + separate SPA on Static Web Apps | API on App Service, SPA on SWA (linked API). Free SSL, global CDN. |
| Azure Static Web Apps with managed Functions | Pure Jamstack-ish, but limits .NET API to Functions. |
| Container (one image with Nginx+Kestrel sidecars) | Use only if your platform demands it. |
Angular vs React vs Blazor — picking one
| Dimension | Angular | React | Blazor (Server/WASM) |
|---|---|---|---|
| Language | TypeScript | TypeScript/JS | C# |
| Opinionation | High (router, forms, HTTP, DI all built-in) | Low (pick libraries) | Medium (built into ASP.NET Core) |
| Reactivity | Signals + RxJS | Hooks + libraries | Component re-render / Blazor signals (preview) |
| Hiring pool | Large in enterprise | Largest overall | Smallest (.NET shops only) |
| Best for | Big LOB apps, strict structure | Nimble UIs, design systems | All-.NET teams; intranet |
| .NET shop fit | Excellent (DI/RxJS map well) | Good (most popular) | Native |
Pick Angular when: existing Angular skill on the team, large form-heavy LOB app, multiple feature teams need a shared structure, you want fewer bikesheds about routing/forms/HTTP.
Code: correct vs wrong
❌ Wrong: subscribing in component without cleanup
ngOnInit() {
this.api.getUsers().subscribe(u => this.users = u); // no unsubscribe → leak on hot streams
}
✅ Correct: async pipe, or takeUntilDestroyed
❌ Wrong: building querystrings by hand
✅ Correct: HttpParams
❌ Wrong: *ngFor without trackBy on large lists
✅ Correct: @for + track
❌ Wrong: storing JWT in localStorage
✅ Correct: BFF + HttpOnly cookie (no JS access)
Server holds tokens; SPA gets a session cookie scoped to the API origin.
Design patterns for this topic
Pattern 1 — "Smart vs dumb components"
- Intent: smart components hold state and inject services; dumb components only take
@Input()and emit@Output(). Keeps re-render scope tight and dumb components reusable.
Pattern 2 — "Facade service"
- Intent: one
XyzFacadeservice exposes signals/observables and methods to a feature, hiding NgRx/HTTP/store choices from components.
Pattern 3 — "Interceptor chain"
- Intent: auth, retry, telemetry, error mapping each as one interceptor. Composable; testable; no per-call boilerplate.
Pattern 4 — "Route-scoped state"
- Intent: provide a
FeatureStorein the lazy route'sprovidersso it lives only while the feature is active.
Pattern 5 — "BFF for SPA auth"
- Intent: SPA never holds tokens. ASP.NET host does OIDC, sets HttpOnly cookie, proxies API calls. Defends against XSS token theft.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Angular framework | Opinionated, batteries-included, scalable | Steeper learning curve than React |
| Signals | Fine-grained reactivity, less RxJS for view state | Two reactivity models (signals + RxJS) coexist |
| RxJS | Powerful streams, async composition | Easy to over-use; subscription leaks |
| NgRx classic | Predictable, devtools, time-travel | Boilerplate-heavy |
| Signal Store | Concise, signal-native | Newer; smaller ecosystem |
| Same-origin (wwwroot) | One deploy, no CORS | Couples release cycles |
| BFF | Best security posture | Extra hop |
| Cross-origin (SWA + App Service) | CDN, scale independently | CORS + token storage decisions |
When to use / when to avoid
- Use Angular for large enterprise apps with multiple teams; form-heavy LOB apps; when the team already knows Angular; when you want strong opinions on routing/forms/HTTP/DI.
- Use React for nimble SPAs, design-system work, broadest hiring pool, when you want to pick your own state/router/HTTP libraries.
- Use Blazor when the team is .NET-only; intranet apps; when sharing C# models server↔client matters more than ecosystem.
- Avoid Angular for small SPAs (< 10 screens) — overkill. Avoid mixing NgRx classic and Signal Store in the same feature.
Interview Q&A
Q1. Standalone components vs NgModules? Standalone components import their own dependencies. Bootstrapped via bootstrapApplication. NgModules deprecated for new code in v18+.
Q2. What's a signal? Reactive primitive (signal, computed, effect) that drives fine-grained change detection — only components reading the changed signal re-render.
Q3. RxJS hot vs cold observable? Cold: each subscriber gets its own producer (e.g. HttpClient.get). Hot: shared producer (e.g. Subject, mouse events).
Q4. Why takeUntilDestroyed()? Auto-unsubscribes when the component is destroyed. Replaces the manual destroy$ = new Subject() + takeUntil(destroy$) boilerplate.
Q5. NgRx vs Signal Store? NgRx: Redux pattern, actions/reducers/effects/selectors, more boilerplate, mature. Signal Store: signal-native, less ceremony, modern. New code → Signal Store unless you have NgRx already.
Q6. Angular DI vs .NET DI? .NET has one container with three lifetimes. Angular has hierarchical injectors — every component creates a child injector. Provide at root for singleton, at component for fresh instance.
Q7. Reactive vs template-driven forms? Template-driven: [(ngModel)], simple, validation in template. Reactive: FormBuilder + FormGroup, code-driven, better for complex/dynamic forms. Reactive is preferred for non-trivial forms.
Q8. Route guards? Functions returning true | UrlTree | Observable<...> that gate navigation. CanActivateFn for pre-nav, CanDeactivateFn for "are you sure" prompts.
Q9. How do interceptors compose? Registered as a chain. Each next(req) passes to the next interceptor. Use for auth, retry, telemetry, error normalization.
Q10. Same-origin vs cross-origin SPA + API? Same-origin: SPA served from .NET host, no CORS. Cross-origin: SPA on CDN, API elsewhere — needs CORS + a token-storage decision. BFF pattern recommended for sensitive apps.
Q11. Why track on @for? Identity function so Angular reuses DOM nodes across re-renders. Without it: full rebuild — perf-killer on large lists.
Q12. Angular vs Blazor for a .NET shop? Blazor: full .NET, no JS. Angular: TypeScript, larger ecosystem, better hiring outside .NET-only firms. Blazor for intranets and small teams; Angular for large LOB apps.
Gotchas / common mistakes
- ⚠️
*ngForwithouttrackBy/@forwithouttrack— full DOM rebuild on every change. - ⚠️ Subscribing in
ngOnInitand never unsubscribing — memory leak on hot streams. Useasyncpipe ortakeUntilDestroyed(). - ⚠️ Storing JWTs in
localStorage— XSS-exfiltrate-able. Prefer BFF + HttpOnly cookie. - ⚠️ Mutating signal values in place (
arr().push(x)) — Angular won't notice. Useupdate/setwith new references. - ⚠️ Mixing NgRx classic and Signal Store in the same feature — pick one.
- ⚠️ Provide a service at root then in a component too — two instances, hard-to-debug behaviour.
- ⚠️ Forgetting
provideHttpClient()in standalone bootstrap — no DI forHttpClient. - ⚠️ Calling RxJS
.toPromise()— deprecated. UsefirstValueFrom/lastValueFrom.