ls -alrtdotnet --versionnode --versionnpm --versionng version
-
dotnet new webapi -n api -
cd api -
dotnet new gitignore -
comment out
app.UseHttpsRedirection();from Program.cs -
run
dotnet new class -o Models -n TodoItemto create model, then updateTodoItemto the followingpublic class TodoItem { public long Id { get; set; } public string? Name { get; set; } public DateTimeOffset? CompletedAt { get; set; } }
-
run
dotnet add package Microsoft.EntityFrameworkCoreto add EF Core to project -
run
dotnet add package Microsoft.EntityFrameworkCore.InMemoryto use EF with In Memory DB -
run
dotnet new class -n ApiDbContextto create db context and update it to bepublic class ApiDbContext: DbContext { public ApiDbContext(DbContextOptions<ApiDbContext> options) : base(options) { } public DbSet<TodoItem> Todos { get; set; } }
-
run
dotnet add package FastEndpointsto add fast endpoints -
update
Program.csto be the followingvar builder = WebApplication.CreateBuilder(args); builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyHeader() .AllowAnyOrigin() .AllowAnyMethod(); }); }); builder.Services.AddOpenApi(); builder.Services.AddFastEndpoints(); builder.Services.AddDbContext<ApiDbContext>(options => options.UseInMemoryDatabase("TodoApiDB")); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseCors("AllowAll"); app.MapFastEndpoints(); app.Run();
-
run
dotnet new class -o Endpoints -n ListTodosEndpointfor List endpointpublic class ListTodosEndpoint(ApiDbContext db) : EndpointWithoutRequest<List<TodoItem>> { public override void Configure() { Get("/api/todos"); Description(b => b .WithTags("Todos") .Produces<List<TodoItem>>(200) .WithSummary("Lists all Todo items") .WithDescription("Lists all Todo items") ); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { var todos = await db.Todos.ToListAsync(ct); await Send.OkAsync(todos, cancellation: ct); } }
-
run
dotnet new class -o Endpoints -n CreateTodoEndpointfor Create endpointpublic class CreateTodoEndpoint(ApiDbContext db) : Endpoint<TodoItem> { public override void Configure() { Post("/api/todos"); Description(b => b .WithTags("Todos") .Produces<TodoItem>(201) .Produces(400) .WithSummary("Creates a new Todo item") .WithDescription("Creates a new Todo item") ); AllowAnonymous(); } public override async Task HandleAsync(TodoItem request, CancellationToken ct) { db.Todos.Add(request); await db.SaveChangesAsync(ct); await Send.CreatedAtAsync($"/api/todos/{request.Id}", request, cancellation: ct); } }
-
run
dotnet new class -o Endpoints -n UpdateTodoEndpointfor Update endpointpublic class UpdateTodoEndpoint(ApiDbContext db) : Endpoint<TodoItem> { public override void Configure() { Put("/api/todos"); Description(b => b .WithTags("Todos") .Produces(204) .Produces(400) .Produces(404) .WithSummary("Updates a Todo item by ID") .WithDescription("Updates a Todo item by ID") ); AllowAnonymous(); } public override async Task HandleAsync(TodoItem request, CancellationToken ct) { var existingTodo = await db.Todos.FirstOrDefaultAsync(t => t.Id == request.Id, ct); if (existingTodo == null) { await Send.NotFoundAsync(cancellation: ct); return; } existingTodo.Name = request.Name; existingTodo.CompletedAt = request.CompletedAt; await db.SaveChangesAsync(ct); await Send.NoContentAsync(cancellation: ct); } }
-
run
dotnet new class -o Endpoints -n DeleteTodoEndpointfor Delete endpointspublic class DeleteTodoEndpoint(ApiDbContext db) : Endpoint<TodoItem> { public override void Configure() { Delete("/api/todos"); Description(b => b .WithTags("Todos") .Produces(204) .Produces(404) .WithSummary("Deletes a Todo item by ID") .WithDescription("Deletes a Todo item by ID") ); AllowAnonymous(); } public override async Task HandleAsync(TodoItem request, CancellationToken ct) { var existingTodo = await db.Todos.FirstOrDefaultAsync(t => t.Id == request.Id, ct); if (existingTodo == null) { await Send.NotFoundAsync(cancellation: ct); return; } db.Todos.Remove(existingTodo); await db.SaveChangesAsync(ct); await Send.NoContentAsync(cancellation: ct); } }
-
run
dotnet run --urls "http://localhost:5000"and navigate to http://localhost:5000/openapi/v1.json on your computer, you should see open api spec -
open
api.httpfile, update with the following code and test APIs@baseUrl = http://localhost:5000 ### List all todos GET {{baseUrl}}/api/todos Accept: application/json ### Create a new todo POST {{baseUrl}}/api/todos Content-Type: application/json Accept: application/json { "name": "Learn FastEndpoints", "completedAt": null } ### Update an existing todo PUT {{baseUrl}}/api/todos Content-Type: application/json Accept: application/json { "id":1, "name": "tetete", "completedAt": null } ### Delete a todo DELETE {{baseUrl}}/api/todos Accept: application/json { "id": 1, "name": "asdasdsad", "completedAt": null }
-
run
ng new web --routing true --style cssto create angular project with N to questions -
run
npm install nswag --save-devto installnswagso we can create FE clients and models -
run
touch nswag.jsonto create config and populate it with{ "runtime": "Net90", "documentGenerator": { "fromDocument": { "url": "http://localhost:5000/openapi/v1.json" } }, "codeGenerators": { "openApiToTypeScriptClient": { "className": "{controller}ServiceProxy", "template": "Angular", "promiseType": "Promise", "httpClass": "HttpClient", "injectionTokenType": "InjectionToken", "generateClientClasses": true, "generateOptionalParameters": true, "rxJsVersion": 7.8, "dateTimeType": "Date", "operationGenerationMode": "MultipleClientsFromPathSegments", "typeStyle": "Class", "baseUrlTokenName": "API_BASE_URL", "output": "src/app/client.ts" } } } -
open
/web/node_modules/nswag/bin/nswag.jsand change default version to Net90, otherwise we have to install dotnet8 sdk#!/usr/bin/env node "use strict"; //var defaultCoreVersion = "Net80"; // <<<REMOVE THIS or COMMENT OUT>>> var defaultCoreVersion = "Net90"; // <<<ADD THIS>>> var supportedCoreVersions = [ { ver: '8.0', dir: "Net80", }, { ver: '9.0', dir: "Net90", }, { ver: '10.0', dir: "Net100", }, ];
-
run
./node_modules/.bin/nswag run nswag.jsonto generate FE client and models -
run
ng generate component pages/todosto create component that will represent the list -
update
app.routes.tsas belowimport { Routes } from '@angular/router'; import { TodosComponent } from './pages/todos/todos.component'; export const routes: Routes = [ { path: '', component: TodosComponent }, ];
-
update
app.config.tsas belowexport const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ApiServiceProxy, { provide: API_BASE_URL, useValue: 'http://localhost:5000' } ] };
-
update
app.component.htmlto be only containing<router-outlet /> -
update
todos.component.tsas below@Component({ selector: 'app-todos', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './todos.component.html', styleUrl: './todos.component.css' }) export class TodosComponent implements OnInit { todos: TodoItem[] = []; selectedTodo: TodoItem | null = null; // used for both create & edit constructor(private api: ApiServiceProxy) { } ngOnInit(): void { this.loadTodos(); } loadTodos() { this.selectedTodo = null; this.api.todosGet().subscribe({ next: (data) => this.todos = data, error: (err) => console.error('Error loading todos:', err) }); } createNew() { this.selectedTodo = new TodoItem({ name: '' }); } edit(todo: TodoItem) { this.selectedTodo = todo; } save() { if (!this.selectedTodo) return; if (this.selectedTodo.id && this.selectedTodo.id > 0) { this.api.todosPut(this.selectedTodo) .subscribe( { next: () => { this.loadTodos(); }, error: (err) => { console.error('Error loading todos:', err) }, } ); } else { this.api.todosPost(this.selectedTodo) .subscribe( { next: () => { this.loadTodos(); }, error: (err) => { console.error('Error loading todos:', err); this.loadTodos(); } } ); } } cancel() { this.selectedTodo = null; } toggleComplete(todo: TodoItem) { todo.completedAt = todo.completedAt ? undefined : new Date(); this.api.todosPut(todo) .subscribe( { next: () => { this.loadTodos(); }, error: (err) => { console.error('Error loading todos:', err) }, }); } delete(todo: TodoItem) { this.api.todosDelete(todo) .subscribe( { next: () => { this.loadTodos(); }, error: (err) => { console.error('Error loading todos:', err) }, }); } }
-
update
todos.component.htmlas below<div class="container"> <h2>Todos</h2> <button (click)="createNew()">Create Todo</button> <ul> <li *ngFor="let todo of todos"> <input type="checkbox" [checked]="todo.completedAt" (change)="toggleComplete(todo)" /> <span [class.completed]="todo.completedAt">{{ todo.name }}</span> <button (click)="edit(todo)">Edit</button> <button (click)="delete(todo)">Delete</button> </li> </ul> <div *ngIf="selectedTodo" class="edit-panel"> <h3>{{ selectedTodo.id ? 'Edit Todo' : 'New Todo' }}</h3> <input [(ngModel)]="selectedTodo.name" placeholder="Todo name" /> <button (click)="save()">Save</button> <button (click)="cancel()">Cancel</button> </div> </div>
-
update
todos.component.cssas below.container { max-width: 600px; margin: 2rem auto; } .add { margin-bottom: 1rem; } .completed { text-decoration: line-through; color: gray; } button { margin-left: 0.5rem; }
-
run angular application with
ng serve --host 0.0.0.0 --port 4200
- devcontainer up --workspace-folder .
- code --folder-uri "vscode-remote://dev-container+<container_id_or_name>/workspace"
- code --folder-uri "vscode-remote://dev-container+vscodedevdays/workspace"
- devcontainer build --workspace-folder . --image-name vscodedevdays
- docker run -d -p 5000:5000 -p 4200:4200 -v $(pwd):/workspaces --name vscodedevdays vscodedevdays:latest