Skip to content

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 build to dist/, copy to wwwroot/ (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>: like Subject but 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 use takeUntilDestroyed() 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)

Action ──► Reducer ──► Store ──► Selector ──► Component
                            Effect (async side-effects)
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.

@Component({ providers: [LocalStateService] })   // new instance per component

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

users$ = this.api.getUsers();   // template: <li *ngFor="let u of users$ | async">
this.api.changes$
  .pipe(takeUntilDestroyed())
  .subscribe(...);

❌ Wrong: building querystrings by hand

this.http.get(`/api/items?q=${query}&page=${page}`);  // no encoding

✅ Correct: HttpParams

this.http.get('/api/items', {
  params: new HttpParams().set('q', query).set('page', page)
});

❌ Wrong: *ngFor without trackBy on large lists

<li *ngFor="let i of items">{{ i.name }}</li>   <!-- full DOM rebuild on every change -->

✅ Correct: @for + track

@for (i of items(); track i.id) { <li>{{ i.name }}</li> }

❌ Wrong: storing JWT in localStorage

localStorage.setItem('token', jwt);   // XSS-exfiltrate-able

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 XyzFacade service 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 FeatureStore in the lazy route's providers so 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

  • ⚠️ *ngFor without trackBy / @for without track — full DOM rebuild on every change.
  • ⚠️ Subscribing in ngOnInit and never unsubscribing — memory leak on hot streams. Use async pipe or takeUntilDestroyed().
  • ⚠️ Storing JWTs in localStorageXSS-exfiltrate-able. Prefer BFF + HttpOnly cookie.
  • ⚠️ Mutating signal values in place (arr().push(x)) — Angular won't notice. Use update/set with 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 for HttpClient.
  • ⚠️ Calling RxJS .toPromise() — deprecated. Use firstValueFrom / lastValueFrom.

Further reading