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— createPUT /api/notes/{id}— update (use RowVersion for optimistic concurrency)GET /api/notes/{id}— get note (respectIsPrivateand owner/permissions)GET /api/notes— list notes with pagination and search queryDELETE /api/notes/{id}— deletePOST /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();
}

Join the conversation! Your thoughts help the community grow.