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
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: true allows 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
# from backend/src/FileViewer.Api
dotnet ef migrations add InitialCreate --project ../FileViewer.Infrastructure --startup-project .
dotnet ef database update --project ../FileViewer.Infrastructure --startup-project .
2) Frontend — Angular app
Tech
Angular 16+ (or your preferred version)
pdfjs-dist for PDF rendering
docx-preview for DOCX → HTML
Native <img>, <video> tags for images / videos
HTML5 drag & drop + FormData uploads 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}`); }
}
File: src/app/services/document-viewer.service.ts
import { Injectable } from '@angular/core';
import * as pdfjsLib from 'pdfjs-dist';
import { renderAsync } from 'docx-preview';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/assets/pdf.worker.min.js';
@Injectable({ providedIn: 'root' })
export class DocumentViewerService {
async renderPdfBlob(blob: Blob, canvasEl: HTMLCanvasElement) {
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1.25 });
canvasEl.width = viewport.width; canvasEl.height = viewport.height;
const ctx = canvasEl.getContext('2d')!;
await page.render({ canvasContext: ctx, viewport }).promise;
}
async renderDocxBlob(blob: Blob, container: HTMLElement) {
await renderAsync(blob, container);
}
}
Component: File Manager (drag & drop)
file-manager.component.html
<div class="file-manager">
<div class="uploader"
(drop)="onDrop($event)"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)">
<p>Drag & drop files here or <input type="file" (change)="onFileSelected($event)" /></p>
</div>
<div class="file-list">
<table class="table">
<thead><tr><th>File</th><th>Size</th><th>Uploaded</th><th></th></tr></thead>
<tbody>
<tr *ngFor="let f of files">
<td><a (click)="preview(f)">{{f.fileName}}</a></td>
<td>{{f.size | number}}</td>
<td>{{f.uploadedOn | date:'short'}}</td>
<td>
<button (click)="download(f)" class="btn btn-sm btn-primary">Download</button>
<button (click)="remove(f)" class="btn btn-sm btn-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- viewer modal -->
<div *ngIf="selected" class="viewer-modal">
<app-file-viewer [meta]="selected" (close)="closePreview()"></app-file-viewer>
</div>
file-manager.component.ts
import { Component, OnInit } from '@angular/core';
import { FileService, FileMeta } from '../../services/file.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-file-manager',
templateUrl: './file-manager.component.html',
styleUrls: ['./file-manager.component.scss']
})
export class FileManagerComponent implements OnInit {
files: FileMeta[] = [];
selected?: FileMeta;
sub?: Subscription;
constructor(private svc: FileService) {}
ngOnInit() { this.load(); }
load() {
this.svc.list().subscribe(x => this.files = x);
}
onDragOver(e: DragEvent) { e.preventDefault(); }
onDragLeave(e: DragEvent) { e.preventDefault(); }
onDrop(e: DragEvent) {
e.preventDefault();
if (!e.dataTransfer) return;
const file = e.dataTransfer.files[0];
if (file) this.upload(file);
}
onFileSelected(evt: any) {
const file: File = evt.target.files[0];
if (file) this.upload(file);
evt.target.value = '';
}
upload(file: File) {
this.svc.upload(file).subscribe({
next: (ev:any) => {
if (ev.type === 4) { // HttpEventType.Response
this.load();
}
},
error: err => console.error(err)
});
}
preview(meta: FileMeta) { this.selected = meta; }
closePreview() { this.selected = undefined; }
download(meta: FileMeta) {
this.svc.download(meta.fileId).subscribe(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = meta.fileName; a.click();
window.URL.revokeObjectURL(url);
});
}
remove(meta: FileMeta) {
if (!confirm('Delete file?')) return;
this.svc.delete(meta.fileId).subscribe(() => this.load());
}
}
file-manager.component.scss (simple)
.file-manager { padding: 12px; max-width: 900px; margin: 16px auto; }
.uploader { border: 2px dashed #ccc; padding: 24px; text-align:center; cursor: pointer; margin-bottom: 12px; }
.viewer-modal { position: fixed; top: 0; left:0; right:0; bottom:0; background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; }
Component: File Viewer
file-viewer.component.ts
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { FileService, FileMeta } from '../../services/file.service';
import { DocumentViewerService } from '../../services/document-viewer.service';
@Component({
selector: 'app-file-viewer',
templateUrl: './file-viewer.component.html',
styleUrls: ['./file-viewer.component.scss']
})
export class FileViewerComponent implements OnInit {
@Input() meta!: FileMeta;
@Output() close = new EventEmitter<void>();
srcUrl?: string;
fileType: 'pdf'|'image'|'video'|'docx'|'other' = 'other';
pdfBlob?: Blob;
docxBlob?: Blob;
constructor(private svc: FileService, private viewer: DocumentViewerService) {}
ngOnInit() {
const ext = (this.meta.fileName.split('.').pop() || '').toLowerCase();
if (ext === 'pdf') this.fileType = 'pdf';
else if (['jpg','jpeg','png','gif','bmp'].includes(ext)) this.fileType = 'image';
else if (['mp4','webm','mov'].includes(ext)) this.fileType = 'video';
else if (ext === 'docx') this.fileType = 'docx';
else this.fileType = 'other';
// fetch blob for viewer
this.svc.download(this.meta.fileId).subscribe(blob => {
if (this.fileType === 'pdf') { this.pdfBlob = blob; }
if (this.fileType === 'docx') { this.docxBlob = blob; }
// for image & video we can createObjectURL
if (this.fileType === 'image' || this.fileType === 'video') {
this.srcUrl = URL.createObjectURL(blob);
}
// render for pdf/docx will be invoked via ViewChild lifecycle after template ready
});
}
onClose() {
if (this.srcUrl) URL.revokeObjectURL(this.srcUrl);
this.close.emit();
}
// methods invoked by template to render pdf/docx by passing canvas/container references
async renderPdf(canvas: HTMLCanvasElement) {
if (this.pdfBlob) await this.viewer.renderPdfBlob(this.pdfBlob, canvas);
}
async renderDocx(container: HTMLElement) {
if (this.docxBlob) await this.viewer.renderDocxBlob(this.docxBlob, container);
}
}
file-viewer.component.html
<div class="viewer">
<div class="viewer-header">
<h5>{{meta.fileName}}</h5>
<button class="btn-close" (click)="onClose()">Close</button>
</div>
<div class="viewer-body">
<div *ngIf="fileType === 'image'">
<img [src]="srcUrl" class="img-fluid" />
</div>
<div *ngIf="fileType === 'video'">
<video [src]="srcUrl" controls style="max-width:100%; height:auto;"></video>
</div>
<div *ngIf="fileType === 'pdf'">
<canvas #pdfCanvas></canvas>
<ng-container *ngIf="pdfBlob">
<!-- call renderPdf after canvas ready -->
<ng-template #t></ng-template>
</ng-container>
</div>
<div *ngIf="fileType === 'docx'">
<div #docxContainer></div>
</div>
<div *ngIf="fileType === 'other'">
<p>Preview not available. <a (click)="download()">Download</a></p>
</div>
</div>
</div>
Use @ViewChild in file-viewer.component.ts to call renderPdf / renderDocx after ViewInit. (I kept code compact; add appropriate lifecycle hook.)
file-viewer.component.scss (UI template)
.viewer { width: 90vw; max-width: 1100px; background: #fff; border-radius: 6px; overflow: auto; padding: 12px; }
.viewer-header { display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #eee; padding-bottom:8px; margin-bottom:12px; }
.viewer-body { padding: 8px; }
.btn-close { background: transparent; border: none; font-size: 18px; cursor: pointer; }
3) UI Design Template (viewer)
Use simple, clean layout with responsive container:
Top bar: file name, close button, download button
Left sidebar (optional): file list, thumbnails
Main area: viewer content (canvas / image / video / docx html)
Footer: page controls (PDF page nav, zoom in/out) — you can extend pdf viewer to support multiple pages
Example main layout (Angular template excerpt):
<div class="viewer-layout">
<aside class="sidebar"> <!-- file list / thumbnails --></aside>
<main class="main-content">
<div class="toolbar">
<button class="btn">Download</button>
<button class="btn">Zoom -</button>
<button class="btn">Zoom +</button>
</div>
<div class="content-area">
<!-- file-viewer component inserted here -->
<app-file-viewer [meta]="selectedMeta"></app-file-viewer>
</div>
</main>
</div>
SCSS (basic)
.viewer-layout { display:flex; height: 90vh; }
.sidebar { width: 260px; border-right: 1px solid #eee; padding: 12px; overflow:auto; }
.main-content { flex:1; display:flex; flex-direction:column; }
.toolbar { padding: 8px; background:#fafafa; border-bottom:1px solid #eee; }
.content-area { flex:1; padding: 12px; overflow:auto; display:flex; align-items:center; justify-content:center; }
4) Drag-and-Drop File Manager module (UX details & behaviour)
Key behaviour:
Drag file into drop zone or click to open file selector
Show upload progress (per-file) and status (success / fail)
After upload, automatically refresh list and generate thumbnails for images & PDF first-page small canvas
Allow bulk uploads (multiple files)
Provide accessibility: keyboard focusable drop zone, fallback file input
Server-side: accept multipart/form-data, write to secure path, return metadata
UX suggestions:
Show small thumbnail column: for images use <img>, for pdf create canvas thumbnail (using pdfjs page render with small scale), for video show poster (first frame extraction is advanced — can use server-side poster generation)
Support drag-selection and keyboard for delete / download
5) Run & Test
Backend
Create solution and projects (core, infrastructure, api) and paste files above.
Install EF packages, create migrations and update DB:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
# create migration (adjust paths)
dotnet ef migrations add InitialCreate --project ../FileViewer.Infrastructure --startup-project .
dotnet ef database update --project ../FileViewer.Infrastructure --startup-project .
Run API:
cd backend/src/FileViewer.Api
dotnet run
Frontend
Install dependencies:
cd frontend/viewer-app
npm install
Copy pdf.worker.min.js from node_modules/pdfjs-dist/build to src/assets/.
Serve Angular:
ng serve
Open Angular app at http://localhost:4200. Upload files via drag & drop and preview.
6) Production Notes & Hardening
Secure file storage: do not expose storage paths. Use tokens for download or signed URLs.
Antivirus / virus scan on upload (especially for DOCX/HTML).
Limit size and validate MIME types and extensions.
Rate limit uploads per IP / API key.
Use CDN for static assets (images, pdf pages) in production.
Use blob storage (Azure Blob / S3) rather than local disk for horizontal scaling. Store only blob URL and metadata in DB.
Authentication & Authorization: protect endpoints with JWT / cookie auth and RBAC.
Logging & Monitoring: record upload/download operations, errors, suspicious attempts.