Below is a ready-to-implement starter project containing:
Complete backend (ASP.NET Core Web API) with file upload/download, streaming, metadata storage (EF Core + SQL Server)
Complete frontend (Angular) with:
Custom File Viewer (PDF, images, video, DOCX) — no
<iframe>UI design template (clean, responsive) for the viewer
Drag-and-drop File Manager module for upload / list / preview / delete
Instructions to run, Docker support, and notes for production hardening
I kept the files minimal and practical so you can run quickly and extend later. All code uses simple, easy-to-understand Indian English.
Repo layout (one-liner)
file-viewer-starter/
├─ backend/
│ ├─ src/
│ │ ├─ FileViewer.Api/ # Web API
│ │ ├─ FileViewer.Core/ # Domain models & DTOs
│ │ └─ FileViewer.Infrastructure/ # EF Core DbContext & migrations
│ └─ docker-compose.yml
├─ frontend/
│ └─ viewer-app/ # Angular app
└─ README.md
1) Backend — ASP.NET Core
Tech
.NET 8, ASP.NET Core Web API
EF Core (SQL Server)
File storage: local disk (can swap to Azure Blob/S3)
Basic endpoints:
POST /api/files/upload— upload file (multipart/form-data)GET /api/files— list metadataGET /api/files/{id}— stream file by idDELETE /api/files/{id}— delete file
Key files (paste into project FileViewer.Api and friends)
File: FileViewer.Core/Models/FileMeta.cs
using System;
namespace FileViewer.Core.Models
{
public class FileMeta
{
public Guid FileId { get; set; } = Guid.NewGuid();
public string FileName { get; set; } = "";
public string ContentType { get; set; } = "";
public long Size { get; set; }
public string StoragePath { get; set; } = ""; // physical path or cloud URL
public string UploadedBy { get; set; } = "";
public DateTime UploadedOn { get; set; } = DateTime.UtcNow;
}
}
File: FileViewer.Infrastructure/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using FileViewer.Core.Models;
namespace FileViewer.Infrastructure
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> opts) : base(opts) { }
public DbSet<FileMeta> Files { get; set; }
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<FileMeta>().HasKey(f => f.FileId);
b.Entity<FileMeta>().Property(f => f.FileName).HasMaxLength(512);
b.Entity<FileMeta>().Property(f => f.StoragePath).HasMaxLength(2000);
}
}
}
File: FileViewer.Api/Controllers/FilesController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FileViewer.Infrastructure;
using FileViewer.Core.Models;
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly IWebHostEnvironment _env;
private readonly ILogger<FilesController> _logger;
public FilesController(ApplicationDbContext db, IWebHostEnvironment env, ILogger<FilesController> logger)
{
_db = db; _env = env; _logger = logger;
}
[HttpGet]
public async Task<IActionResult> List()
{
var list = await _db.Files.OrderByDescending(f => f.UploadedOn).ToListAsync();
return Ok(list);
}
[HttpPost("upload")]
[RequestSizeLimit(200_000_000)] // allow ~200MB; adjust for your needs
public async Task<IActionResult> Upload([FromForm] IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest("No file uploaded");
var uploads = Path.Combine(_env.ContentRootPath, "uploads");
if (!Directory.Exists(uploads)) Directory.CreateDirectory(uploads);
var fileId = Guid.NewGuid();
var safeName = Path.GetFileName(file.FileName);
var ext = Path.GetExtension(safeName);
var storedName = $"{fileId}{ext}";
var path = Path.Combine(uploads, storedName);
using (var stream = System.IO.File.Create(path))
{
await file.CopyToAsync(stream);
}
var meta = new FileMeta {
FileId = fileId,
FileName = safeName,
ContentType = file.ContentType ?? "application/octet-stream",
Size = file.Length,
StoragePath = path
};
_db.Files.Add(meta);
await _db.SaveChangesAsync();
return Ok(meta);
}
[HttpGet("{id}")]
public async Task<IActionResult> Download(Guid id)
{
var meta = await _db.Files.FindAsync(id);
if (meta == null) return NotFound();
var stream = System.IO.File.OpenRead(meta.StoragePath);
return File(stream, meta.ContentType, meta.FileName, enableRangeProcessing: true);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
var meta = await _db.Files.FindAsync(id);
if (meta == null) return NotFound();
try {
if (System.IO.File.Exists(meta.StoragePath)) System.IO.File.Delete(meta.StoragePath);
} catch (Exception ex) {
_logger.LogWarning(ex, "delete file failed");
}
_db.Files.Remove(meta);
await _db.SaveChangesAsync();
return NoContent();
}
}
Note:
enableRangeProcessing: trueallows client to seek video streams.
File: FileViewer.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// EF Core configuration - read connection string from appsettings
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddCors(o => o.AddPolicy("AllowLocal", p =>
{
p.AllowAnyHeader().AllowAnyMethod().AllowCredentials().WithOrigins("http://localhost:4200");
}));
var app = builder.Build();
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }
app.UseHttpsRedirection();
app.UseCors("AllowLocal");
app.UseAuthorization();
app.MapControllers();
app.Run();
appsettings.Development.json (example)
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=FileViewerDb;Trusted_Connection=True;"
}
}
Docker support & migrations
Add EF packages to
FileViewer.InfrastructureandFileViewer.Apiprojects and run migrations:
# from backend/src/FileViewer.Api
dotnet ef migrations add InitialCreate --project ../FileViewer.Infrastructure --startup-project .
dotnet ef database update --project ../FileViewer.Infrastructure --startup-project .
Use
docker-compose.ymlto run SQL Server for parity (similar pattern to previous answers).
2) Frontend — Angular app
Tech
Angular 16+ (or your preferred version)
pdfjs-distfor PDF renderingdocx-previewfor DOCX → HTMLNative
<img>,<video>tags for images / videosHTML5 drag & drop +
FormDatauploads for file manager
Install dependencies:
ng new viewer-app
cd viewer-app
npm install pdfjs-dist docx-preview @types/pdfjs-dist
npm install bootstrap --save # optional UI framework
Important: Add pdf worker file
Copy node_modules/pdfjs-dist/build/pdf.worker.min.js to src/assets/ and set worker path in code.
Frontend file list (key files)
viewer-app/
└─ src/app/
├─ services/file.service.ts # API calls
├─ services/document-viewer.service.ts
├─ components/file-manager/ # drag-drop uploader + list
│ ├─ file-manager.component.ts
│ ├─ file-manager.component.html
│ └─ file-manager.component.scss
├─ components/file-viewer/ # unified viewer component
│ ├─ file-viewer.component.ts
│ ├─ file-viewer.component.html
│ └─ file-viewer.component.scss
└─ app.module.ts
File: src/app/services/file.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface FileMeta {
fileId: string;
fileName: string;
contentType: string;
size: number;
uploadedOn: string;
}
@Injectable({ providedIn: 'root' })
export class FileService {
private base = '/api/files';
constructor(private http: HttpClient) {}
list(): Observable<FileMeta[]> { return this.http.get<FileMeta[]>(this.base); }
upload(file: File) {
const fd = new FormData();
fd.append('file', file, file.name);
return this.http.post<FileMeta>(`${this.base}/upload`, fd, { reportProgress: true, observe: 'events' });
}
download(fileId: string) {
return this.http.get(`${this.base}/${fileId}`, { responseType: 'blob' });
}
delete(fileId: string) { return this.http.delete(`${this.base}/${fileId}`); }
}

Join the conversation! Your thoughts help the community grow.