ASP.NET Core  

Notes App — With Rich Text Editor (ASP.NET Core + Angular)

Introduction

This article shows how to build a production-minded Notes application that supports rich-text editing, saving, and searching. The backend uses ASP.NET Core (Web API) with EF Core and the frontend is an Angular SPA. The editor used in examples is Quill (open-source, lightweight) but I also explain how to swap in CKEditor or TinyMCE.

The guide is written in simple Indian English and targets senior developers who want a clear, copy-pasteable implementation with best practices for security, storage, collaboration, and search.

What you will learn

  • Project structure for backend and frontend

  • EF Core models, migrations and storage for rich text

  • Angular integration with Quill editor (reactive forms)

  • Sanitisation, search, and attachments

  • Collaboration basics (optional SignalR)

  • Production considerations: backups, XSS protection, indexing, and performance

High-level architecture

[Angular SPA] <-- HTTPS/JWT --> [ASP.NET Core Web API] <---> [SQL Server]
      |                                        |
      |                                        +--> [Blob Storage for attachments]
      +--(optional SignalR for live-collab)-->

Notes content is stored as HTML (produced by the rich-text editor). We must ensure server-side sanitisation before persisting to avoid XSS when rendering notes later.

Database Design (simple)

Keep the schema small and extensible.

  • Users(Id, UserName, Email, PasswordHash, CreatedAt)

  • Notes(Id, OwnerId, Title, ContentHtml, ContentText, IsPrivate, CreatedAt, UpdatedAt, Version)

  • NoteAttachments(Id, NoteId, FileName, ContentType, BlobUrl, Size, CreatedAt)

Notes explanation

  • ContentHtml: stores rich HTML from editor.

  • ContentText: plain-text extract used for search and previews.

  • Version: a concurrency token (rowversion or integer) to support optimistic concurrency.

Add indexes on OwnerId, UpdatedAt, and a full-text index on ContentText (or use an external search engine for better results).

Backend: Project setup and packages

Create project

dotnet new webapi -n NotesBackend
cd NotesBackend

Install packages

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Ganss.XSS        // for sanitisation
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Ganss.XSS is a simple and trusted HTML sanitizer for .NET. You can swap or add your own policies.

Backend: EF Core model examples

Note.cs

public class Note
{
    public Guid Id { get; set; }
    public Guid OwnerId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string ContentHtml { get; set; } = string.Empty;
    public string ContentText { get; set; } = string.Empty; // plain text for search
    public bool IsPrivate { get; set; } = true;
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
    public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
    [Timestamp]
    public byte[]? Version { get; set; }

    public List<NoteAttachment> Attachments { get; set; } = new();
}

NoteAttachment.cs

public class NoteAttachment
{
    public Guid Id { get; set; }
    public Guid NoteId { get; set; }
    public string FileName { get; set; } = string.Empty;
    public string ContentType { get; set; } = string.Empty;
    public string BlobUrl { get; set; } = string.Empty;
    public long Size { get; set; }
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}

Register DbSet<Note> and DbSet<NoteAttachment> in NotesDbContext and create migrations.

Backend: Sanitise HTML before saving

Never trust editor HTML. Use a sanitizer.

public class NoteService : INoteService
{
    private readonly NotesDbContext _db;
    private readonly HtmlSanitizer _sanitizer; // Ganss.XSS

    public NoteService(NotesDbContext db)
    {
        _db = db;
        _sanitizer = new HtmlSanitizer();
        _sanitizer.AllowedSchemes.Add("data"); // if you allow inline images
    }

    public async Task<NoteDto> CreateOrUpdateNoteAsync(Guid userId, NoteEditDto dto)
    {
        var cleanHtml = _sanitizer.Sanitize(dto.ContentHtml ?? string.Empty);
        var plainText = HtmlUtilities.ConvertToPlainText(cleanHtml); // simple helper

        // create or update entity
        var note = // mapping ...

        note.ContentHtml = cleanHtml;
        note.ContentText = plainText;

        // save and return
    }
}

HtmlUtilities.ConvertToPlainText can be a small helper that strips tags and decodes entities. Store this for search.

Backend: API endpoints

Design a small API surface:

  • POST /api/notes — create

  • PUT /api/notes/{id} — update (use RowVersion for optimistic concurrency)

  • GET /api/notes/{id} — get note (respect IsPrivate and owner/permissions)

  • GET /api/notes — list notes with pagination and search query

  • DELETE /api/notes/{id} — delete

  • POST /api/notes/{id}/attachments — upload attachment

Example: Update note with concurrency

[HttpPut("{id}")]
public async Task<IActionResult> Update(Guid id, [FromBody] NoteEditDto dto)
{
    var userId = User.GetUserId();
    var note = await _db.Notes.FindAsync(id);
    if (note == null) return NotFound();
    if (note.OwnerId != userId) return Forbid();

    // sanitize
    var clean = _sanitizer.Sanitize(dto.ContentHtml);
    note.Title = dto.Title;
    note.ContentHtml = clean;
    note.ContentText = HtmlUtilities.ConvertToPlainText(clean);
    note.UpdatedAt = DateTimeOffset.UtcNow;
    _db.Entry(note).Property("Version").OriginalValue = dto.Version; // byte[]

    try
    {
        await _db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        return Conflict(new { message = "Note was modified by another session" });
    }

    return NoContent();
}

Best practice: Return 409 Conflict if concurrency fails and provide current server version.

Frontend: Angular Setup and Editor choice

Create Angular app and add Quill:

ng new notes-app
cd notes-app
npm install quill ngx-quill @types/quill

ngx-quill integrates Quill with Angular forms. If you prefer CKEditor, @ckeditor/ckeditor5-angular is also a good choice.

Frontend: NoteEditComponent (Reactive form with Quill)

note-edit.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({ selector: 'app-note-edit', templateUrl: './note-edit.component.html' })
export class NoteEditComponent implements OnInit {
  form!: FormGroup;
  editorModules = {
    toolbar: [
      ['bold', 'italic', 'underline', 'strike'],
      [{ 'header': [1, 2, 3, false] }],
      [{ 'list': 'ordered'}, { 'list': 'bullet' }],
      ['link', 'image', 'blockquote']
    ]
  };

  constructor(private fb: FormBuilder, private api: NotesApiService) {}

  ngOnInit() {
    this.form = this.fb.group({
      title: ['', [Validators.required, Validators.maxLength(200)]],
      contentHtml: ['', Validators.required],
      isPrivate: [true]
    });
  }

  async save() {
    if (this.form.invalid) return;
    const dto = this.form.value;
    await this.api.saveNote(dto);
  }
}

note-edit.component.html

<form [formGroup]="form" (ngSubmit)="save()">
  <input formControlName="title" placeholder="Title" />

  <quill-editor formControlName="contentHtml" [modules]="editorModules"></quill-editor>

  <label><input type="checkbox" formControlName="isPrivate" /> Private</label>

  <button type="submit">Save</button>
</form>

Notes

  • Use reactive forms so you can handle validation and autosave easily.

  • Configure Quill modules to limit allowed formats.

Frontend: Sanitisation and Preview

Client-side sanitisation is useful for better UX, but it must not replace server sanitisation.

For preview, use Angular's DomSanitizer to safely render HTML:

constructor(private sanitizer: DomSanitizer) {}

getSafeHtml(html: string) {
  return this.sanitizer.bypassSecurityTrustHtml(html);
}

Important: bypassSecurityTrustHtml does not sanitise — it marks the HTML as trusted for Angular's DOM. Always send clean HTML from server. Only bypass when you have sanitized on server or you trust the source.

Search: Simple vs Advanced

Simple: store ContentText plain text and use SQL LIKE searches or full-text index (recommended for moderate scale).

Advanced: use Elasticsearch or Azure Cognitive Search for better ranking and fuzzy search.

Example SQL full-text search query:

SELECT * FROM Notes WHERE CONTAINS(ContentText, '"meeting" OR "notes"')

Attachments

Upload attachments to blob storage. On upload:

  1. Validate file size and type.

  2. Upload to blob storage.

  3. Store metadata in NoteAttachments.

  4. Optionally create image thumbnails for preview.

Angular upload example

const fd = new FormData();
fd.append('file', file);
this.http.post(`/api/notes/${noteId}/attachments`, fd, { reportProgress: true, observe: 'events' })

Autosave and Drafts

Implement autosave with debounce to avoid excessive saves:

this.form.valueChanges.pipe(debounceTime(2000)).subscribe(value => this.saveDraft(value));

On server, keep a Drafts table or a flag on Notes marking unpublished drafts. Autosave must track Version for concurrency.

Collaboration (optional)

For live-collaboration (multiple editors on same note), use an operational transform (OT) or CRDT library. Quill has OT support via ShareDB and other infrastructures.

Simpler approach: implement presence and remote cursors via SignalR and broadcast deltas. This is non-trivial and outside a small app scope; for production collaborative editing prefer established services or libraries.

Security considerations

  • Server-side sanitisation: always sanitize HTML.

  • Content-length and upload limits: limit size to protect the server.

  • Authentication and authorization: owners can edit; others need explicit share permissions.

  • XSS protection: escape content where appropriate and sanitize embedded URLs.

  • Rate limiting: prevent abuse from automated scripts.

  • Backups: schedule regular DB and blob backups.

Performance and Storage

  • Store large attachments in blob store, not DB.

  • Keep ContentHtml in DB if small; else use blob store for very large notes and store pointer in DB.

  • Use pagination and limit content in note lists (fetch only title and content snippet).

Monitoring and Observability

Track:

  • Save failures and conflicts

  • Upload failures and storage usage

  • Search latency and errors

  • Autosave frequency and errors

Use Application Insights, Prometheus + Grafana, or ELK stack for logs and metrics.

Deployment checklist

  • Use HTTPS and secure JWT keys via key vault

  • Configure CORS strictly

  • Configure DB connection pooling

  • Run EF migrations during deployment

  • Configure blob storage credentials securely

Example: Minimal NoteController (ASP.NET Core)

[ApiController]
[Route("api/[controller]")]
public class NotesController : ControllerBase
{
    private readonly INoteService _service;

    public NotesController(INoteService service)
    {
        _service = service;
    }

    [HttpPost]
    [Authorize]
    public async Task<IActionResult> Create([FromBody] NoteEditDto dto)
    {
        var userId = User.GetUserId();
        var note = await _service.CreateOrUpdateNoteAsync(userId, dto);
        return CreatedAtAction(nameof(Get), new { id = note.Id }, note);
    }

    [HttpGet("{id}")]
    [Authorize]
    public async Task<IActionResult> Get(Guid id)
    {
        var userId = User.GetUserId();
        var note = await _service.GetNoteAsync(userId, id);
        if (note == null) return NotFound();
        return Ok(note);
    }
}

Final best practices (short list)

  • Sanitize at server; never rely only on client.

  • Use optimistic concurrency for edits.

  • Keep attachments in blob storage.

  • Implement autosave with debounce and conflict detection.

  • Use full-text search or dedicated search engine for larger data sets.

  • Secure uploads and sanitize URLs and images.

Next steps

I can now:

  • generate a ready-to-run GitHub repository scaffold (backend + frontend),

  • add EF Core migrations and seed sample data,

  • provide a working Angular component set with unit tests,

  • add example SignalR live-collaboration scaffold,

  • or produce PNG/SVG diagrams for architecture and flow.

Tell me which one you want next.