Building a Scalable, Real-World Full-Stack Application
Image recognition has become an essential feature in many modern applications. From security systems and document scanning to e-commerce and social media platforms, the ability to detect and classify images in real-time adds tremendous value. Combining Angular on the frontend with an ASP.NET Core backend is a powerful approach for building a scalable image recognition application.
In this article, we will discuss a practical approach to integrating image recognition into an Angular application using an ASP.NET Core backend. We will cover architectural considerations, implementation strategies, performance optimizations, and production-ready best practices.
1. Architecture Overview
A high-level architecture of the system looks like this:
Angular Frontend
|
| Upload Image / Fetch Results
v
ASP.NET Core API
|
| Calls Image Recognition Model / Service
v
Machine Learning Service
|
| Returns Prediction
v
ASP.NET Core API
|
v
Angular Frontend
1.1 Frontend Responsibilities
Handle user image uploads.
Display processed results.
Show loading state and progress.
Perform client-side validation (file type, size, resolution).
1.2 Backend Responsibilities
Accept image uploads.
Validate image integrity.
Interface with the image recognition model or service (e.g., TensorFlow, ML.NET, OpenCV).
Return predictions as structured JSON.
Optionally, store images and results in database or cloud storage.
1.3 Machine Learning / Recognition Layer
Can be ML.NET for .NET-centric pipelines.
TensorFlow or PyTorch REST API is also a viable option.
Ensure inference is scalable and does not block the API thread.
2. Angular Frontend Implementation
2.1 Creating the Upload Component
We create a component to allow the user to select an image and upload it.
// image-upload.component.ts
import { Component } from '@angular/core';
import { ImageService } from '../services/image.service';
@Component({
selector: 'app-image-upload',
templateUrl: './image-upload.component.html',
styleUrls: ['./image-upload.component.css']
})
export class ImageUploadComponent {
selectedFile!: File;
loading = false;
prediction: any;
constructor(private imageService: ImageService) {}
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0];
}
}
uploadImage() {
if (!this.selectedFile) return;
this.loading = true;
this.imageService.uploadImage(this.selectedFile).subscribe({
next: (result) => {
this.prediction = result;
this.loading = false;
},
error: (err) => {
console.error(err);
this.loading = false;
}
});
}
}
2.2 HTML Template
<div class="upload-container">
<input type="file" (change)="onFileSelected($event)" accept="image/*" />
<button (click)="uploadImage()" [disabled]="!selectedFile || loading">
Upload
</button>
<div *ngIf="loading">Processing...</div>
<div *ngIf="prediction">
<h3>Prediction Result</h3>
<p>{{ prediction.label }} - Confidence: {{ prediction.confidence | percent }}</p>
</div>
</div>
2.3 Image Service
// image.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ImageService {
private apiUrl = 'https://localhost:5001/api/images';
constructor(private http: HttpClient) {}
uploadImage(file: File): Observable<any> {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post<any>(`${this.apiUrl}/recognize`, formData);
}
}
2.4 Angular Best Practices
Validation: Restrict allowed MIME types and file sizes on client-side before upload.
Loading States: Use a spinner or progress bar.
Error Handling: Provide user-friendly error messages.
OnPush Change Detection: Use OnPush for performance when handling streaming or large results.
Async Pipe: Avoid manual subscriptions if possible; for this example, manual subscription is fine for upload-and-receive patterns.
3. ASP.NET Core Backend Implementation
3.1 Creating a Controller
// Controllers/ImagesController.cs
using Microsoft.AspNetCore.Mvc;
using ImageRecognitionAPI.Services;
namespace ImageRecognitionAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ImagesController : ControllerBase
{
private readonly IImageRecognitionService _recognitionService;
public ImagesController(IImageRecognitionService recognitionService)
{
_recognitionService = recognitionService;
}
[HttpPost("recognize")]
public async Task<IActionResult> Recognize(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");
try
{
var result = await _recognitionService.RecognizeAsync(file);
return Ok(result);
}
catch (Exception ex)
{
// Log exception
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
}
3.2 Image Recognition Service
For simplicity, we can use ML.NET or a Python service. Here's an ML.NET example:
// Services/ImageRecognitionService.cs
using Microsoft.ML;
using Microsoft.ML.Data;
namespace ImageRecognitionAPI.Services
{
public interface IImageRecognitionService
{
Task<PredictionResult> RecognizeAsync(IFormFile file);
}
public class ImageRecognitionService : IImageRecognitionService
{
private readonly MLContext _mlContext;
private readonly ITransformer _model;
public ImageRecognitionService()
{
_mlContext = new MLContext();
// Load pre-trained ONNX or ML.NET model
_model = _mlContext.Model.Load("Model.zip", out var inputSchema);
}
public async Task<PredictionResult> RecognizeAsync(IFormFile file)
{
using var stream = new MemoryStream();
await file.CopyToAsync(stream);
var imageData = new ImageData { Image = stream.ToArray() };
var predictor = _mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(_model);
var prediction = predictor.Predict(imageData);
return new PredictionResult
{
Label = prediction.PredictedLabel,
Confidence = prediction.Score.Max()
};
}
}
public class ImageData
{
[ColumnName("input")]
public byte[] Image { get; set; }
}
public class ImagePrediction
{
[ColumnName("output_label")]
public string PredictedLabel { get; set; }
[ColumnName("output_score")]
public float[] Score { get; set; }
}
public class PredictionResult
{
public string Label { get; set; }
public float Confidence { get; set; }
}
}
3.3 Production Considerations
Model Loading: Load the model once during service startup, not per request.
Async Processing: Use Task.Run for CPU-bound inference if multiple requests arrive simultaneously.
Memory Management: Dispose of streams and unmanaged resources properly.
Validation: Check image dimensions and type before processing.
Logging: Use structured logging for observability.
4. Real-Time Upload and Progress Feedback
For large images, consider Angular’s HttpClient upload progress.
uploadImage() {
const formData = new FormData();
formData.append('file', this.selectedFile);
this.imageService.uploadImageWithProgress(formData).subscribe({
next: event => {
if (event.type === HttpEventType.UploadProgress) {
this.progress = Math.round((event.loaded / event.total!) * 100);
} else if (event.type === HttpEventType.Response) {
this.prediction = event.body;
this.progress = 0;
}
},
error: err => console.error(err)
});
}
Backend should support streaming or large uploads with RequestSizeLimit.
5. Security and Best Practices
File Validation: Never trust file extensions; check MIME type and magic bytes.
File Size Limit: Configure MaxRequestBodySize to prevent DoS.
Sanitisation: If saving files temporarily, use a secure temp directory.
CORS Policy: Angular frontend may run on a different domain; configure CORS properly.
Authentication: Use JWT or cookie-based auth for secure endpoints.
Rate Limiting: Protect recognition endpoints against abuse.
6. Performance and Scalability Tips
Batch Processing: For multiple images, process in batches asynchronously.
Caching Results: Cache predictions for repeated images.
Offload Heavy Models: Consider using GPU-accelerated servers or a dedicated ML microservice.
Async Queue: Use a queue like RabbitMQ or Azure Service Bus for high-load scenarios.
WebSockets: For real-time result streaming to Angular without polling.
7. Optional Enhancements
Drag-and-Drop Upload: Enhance UX using Angular CDK or libraries like ngx-file-drop.
Preview Image: Display the selected image before upload.
Multiple Model Support: Let backend select models dynamically based on context.
Image Annotation: Highlight detected objects in frontend using Canvas or SVG overlays.
Mobile Responsiveness: Ensure Angular component works for touch and small screens.
8. Testing
Unit Tests
Integration Tests
Performance Testing
Upload multiple large images to simulate production load.
Measure CPU, memory, and response latency.
9. Deployment Considerations
Angular: Build with ng build --prod, serve via Nginx, Apache, or ASP.NET Core static files.
ASP.NET Core API: Deploy on Kestrel behind reverse proxy (Nginx/Apache/IIS).
HTTPS: Ensure TLS for image upload endpoints.
Cloud Storage: For heavy image workloads, store images on AWS S3, Azure Blob Storage, or GCP Cloud Storage.
Containerization: Dockerize frontend and backend for easier deployment and scaling.
Summary
By combining Angular with an ASP.NET Core backend, we can build a production-ready image recognition application that is:
User-Friendly: Smooth image upload and real-time predictions.
Scalable: Proper async processing, batch support, and queue integration.
Secure: File validation, authentication, and rate limiting.
Maintainable: Clear separation between frontend, API, and ML service layers.
Key takeaways for senior developers:
Use Angular services and async pipes effectively to handle image upload streams.
Keep backend stateless and async for scalability.
Load ML models once and avoid per-request initialization.
Implement throttling or batch processing for high-frequency image recognition requests.
Monitor performance, memory, and request latency continuously.
With these practices, you can build enterprise-grade image recognition features in Angular applications with an ASP.NET Core backend.