Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- name: Test
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
pnpm nx affected --target=test --base=$NX_BASE --configuration ci
pnpm nx affected --target=test --base=$NX_BASE
else
pnpm test:all
fi
Expand Down Expand Up @@ -80,3 +80,34 @@ jobs:
name: test-results
path: junit/
if-no-files-found: ignore

- name: Coverage summary
if: always()
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: coverage/**/cobertura-coverage.xml
badge: true
fail_below_min: false
format: markdown
output: both
thresholds: '60 80'

- name: Annotate coverage report
if: always() && github.event_name == 'pull_request'
run: |
echo "> [!NOTE]" > coverage-note.md
echo "> Coverage shown for **affected projects only**. Per-file thresholds (80%) are enforced by vitest." >> coverage-note.md
echo "" >> coverage-note.md
cat code-coverage-results.md >> coverage-note.md
mv coverage-note.md code-coverage-results.md

- name: Write coverage to job summary
if: always()
run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY

- name: Add coverage PR comment
if: always() && github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
recreate: true
path: code-coverage-results.md
8 changes: 8 additions & 0 deletions .lintstagedrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
'*.{ts,js}': 'eslint --cache --cache-location=.husky/_ --fix',
'*.{ts,js,css,scss,md,mdx}': 'prettier --write',
'README.md': () => [
'pnpm nx run web-app:update-readme',
'git add apps/web-app/src/assets/home.md',
],
};
94 changes: 57 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,98 @@
[![Build status](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_apis/build/status/chrisjwalk.angular-cli-netcore-ngrx-starter)](https://freshpondmedia.visualstudio.com/FreshPondMediaGit/_build/latest?definitionId=43)
[![CI](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/chrisjwalk/angular-cli-netcore-ngrx-starter/actions/workflows/ci.yml)
Comment thread
chrisjwalk marked this conversation as resolved.

# Nx + Angular + .NET 10.0

This is basic demo of how to use a full stack [Nx monorepo](https://nx.dev/getting-started/tutorials/angular-monorepo-tutorial) with [Angular](https://angular.dev) and .NET 10.0 with [Microsoft.AspNetCore.SpaServices.Extensions](https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/angular) and a demo Azure pipeline for Azure DevOps.
A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://angular.dev) (zoneless, signals) and a .NET 10.0 Web API backend. Deployed to Azure App Service with automated PR preview deployments via Azure Static Web Apps.

## Demo
## Features

See a live demo here: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/)
- **Authentication** — register, login, and logout with JWT bearer tokens backed by ASP.NET Core Identity
- **Notification centre** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update)
- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload
- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development
- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps

## Getting Started?
## Tech stack

- **Make sure you have at least Node 24.x or higher (w/ pnpm 10+) installed!**
- **This repository uses ASP.NET 10.0, which has a hard requirement on .NET SDK 10.0.x. Please install these items from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download)**
**Frontend**

## Visual Studio 2022
- [Angular 21](https://angular.dev) — zoneless change detection, standalone components, signals
- [NgRx Signal Store](https://ngrx.io/guide/signals) — reactive state management
- [Angular Material](https://material.angular.io) — UI component library
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling
- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support

Make sure you have .NET 10.0 installed and/or the latest VS2026.
**Backend**

## Visual Studio Code
- [.NET 10.0](https://dotnet.microsoft.com) Web API
- [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity) — bearer token authentication
- [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) with Azure SQL

> Note: Make sure you have the C# extension & .NET Debugger installed.
**Tooling**

pnpm install
- [Nx](https://nx.dev) — monorepo build system with affected commands
- [Vitest](https://vitest.dev) — unit tests with ~93% line coverage
- [Playwright](https://playwright.dev) — end-to-end tests
- [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged) — pre-commit hooks for linting, formatting, and keeping `home.md` in sync
- [pnpm](https://pnpm.io) — package manager

## Serve Development App
## Demo

```
pnpm start
```
Live demo: [https://angularclinetcorengrxstarter.azurewebsites.net/](https://angularclinetcorengrxstarter.azurewebsites.net/)

## Getting started

Both the api (dotnet) and web app (Angular) will build and run in dev mode. Open your browser on http://localhost:4200/ to see the Angular app, or https://localhost:60254/swagger to see the api documentation generated by Swagger.
**Prerequisites**

## Serve Production App (PWA enabled)
- Node 24.x+ with pnpm 10+
- .NET SDK 10.0.x — [download](https://dotnet.microsoft.com/download)

**Install dependencies**

```bash
pnpm install
```
pnpm serve:prod

## Serve development app

```bash
pnpm start
```

Starts both the .NET API and Angular app in dev mode. Open [http://localhost:4200](http://localhost:4200) for the app, or [https://localhost:60254/swagger](https://localhost:60254/swagger) for the API docs.

## Lint

```
```bash
pnpm lint
```

## Unit Tests

Run unit tests by executing:
## Unit tests

```
```bash
pnpm test
```

Please note that for test coverage you need dotnet-coverage to be installed:
Coverage requires `dotnet-coverage`:

```
```bash
dotnet tool install --global dotnet-coverage
```

More information is available on https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test

## End-to-end Tests

Run e2e tests by executing:
## End-to-end tests

```
```bash
pnpm e2e
```

## Build Production App

Build the production Angular app and Publish the release .NET app, run:
## Build for production

```
```bash
pnpm build:prod
```

The contents of the. `/dist` folder should now contain something that can be deployed to and Azure web service or IIS instance.
Builds the Angular app and publishes the .NET project to `/dist`, ready to deploy to Azure App Service.

## Contributing

`apps/web-app/src/assets/home.md` is auto-generated from this file — **edit `README.md` only**. The lint-staged hook regenerates `home.md` automatically whenever `README.md` is committed.
2 changes: 1 addition & 1 deletion apps/web-app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "cat docs/github-readme.md > README.md && cat apps/web-app/src/assets/home.md >> README.md"
"command": "sed -n '/^# /,$p' README.md > apps/web-app/src/assets/home.md"
}
}
}
Expand Down
77 changes: 77 additions & 0 deletions apps/web-app/src/app/debug/debug.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { NotificationStore } from '@myorg/shared';
import { fireEvent, render, screen } from '@testing-library/angular';

import { Debug } from './debug';

async function setup() {
const { fixture } = await render(Debug, {
providers: [provideNoopAnimations(), NotificationStore],
});
const store = fixture.componentInstance.store;
return { store };
}

describe('Debug', () => {
it('should render debug tools heading', async () => {
await setup();
expect(screen.getByText(/debug tools/i)).toBeTruthy();
});

it('addInfo should add an info notification', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add info'));
expect(store.notifications().length).toBe(1);
expect(store.notifications()[0].kind).toBe('info');
});

it('addError should add an error notification', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add error'));
expect(store.notifications()[0].kind).toBe('error');
});

it('addAuth should add an auth notification', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add auth'));
expect(store.notifications()[0].kind).toBe('auth');
});

it('addSwUpdate should add a sw-update notification with reload action', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add sw-update'));
const notification = store.notifications()[0];
expect(notification.kind).toBe('sw-update');
expect(notification.action?.label).toBe('Reload');
});

it('addWithAction should add a notification with an action', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add with action'));
expect(store.notifications()[0].action?.label).toBe('Do it');
});

it('addAutoDismiss should add a notification with autoDismissMs', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add auto-dismiss (3s)'));
expect(store.notifications()[0].autoDismissMs).toBe(3000);
});

it('clearAll should dismiss all notifications', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add info'));
fireEvent.click(screen.getByText('Add error'));
expect(store.notifications().length).toBe(2);
fireEvent.click(screen.getByText('Clear all'));
expect(store.notifications().length).toBe(0);
});

it('mark all read should mark all notifications as read', async () => {
const { store } = await setup();
fireEvent.click(screen.getByText('Add info'));
fireEvent.click(screen.getByText('Add error'));
expect(store.unreadCount()).toBe(2);
fireEvent.click(screen.getByText('Mark all read'));
expect(store.unreadCount()).toBe(0);
});
});
Loading
Loading