![frontent]()
Previous article: Blazor Superpowers - SPA Components Real Time - ASP.NET Core - Master WebApps (Part-22 of 40)
Table of contents
Understanding Frontend-Backend Architecture
Integration Patterns Overview
React + ASP.NET Core Deep Integration
Angular + ASP.NET Core Enterprise Setup
Vue.js + ASP.NET Core Lightweight Fusion
Real-time Communication Strategies
State Management Across Boundaries
Authentication & Authorization Flow
Performance Optimization Techniques
Deployment & DevOps Strategies
Table of Contents
Introduction to Frontend Fusion
Architecture Patterns
Project Setup & Configuration
React Integration Deep Dive
Angular Enterprise Integration
Vue.js Lightweight Integration
Real-World E-Commerce Example
Performance Optimization
Security Considerations
Deployment Strategies
1. Introduction to Frontend Fusion
The Modern Web Development Landscape
In today's web development ecosystem, the separation of frontend and backend has become a standard practice. However, seamlessly integrating these two worlds remains a challenge that many developers face. Frontend Fusion represents the art and science of blending JavaScript frameworks with ASP.NET Core to create powerful, maintainable, and scalable applications.
Why Frontend Fusion Matters
Traditional Approach Limitations:
Tight coupling between UI and business logic
Limited scalability
Poor developer experience
Difficulty in adopting new frontend technologies
Frontend Fusion Benefits:
Real-World Scenario: E-Commerce Platform
Imagine building an e-commerce platform where:
Product catalog uses React for rich interactivity
Admin dashboard uses Angular for enterprise features
Customer portal uses Vue.js for lightweight performance
All seamlessly integrated with ASP.NET Core backend
2. Architecture Patterns
2.1 Separation of Concerns Architecture
// Backend Architecture
ECommerceSolution/
├── ECommerce.API/ // ASP.NET Core Web API
├── ECommerce.Application/ // Business Logic
├── ECommerce.Domain/ // Domain Models
├── ECommerce.Infrastructure/ // Data Access
└── ECommerce.Shared/ // Shared Utilities
// Frontend Architecture
frontend/
├── react-app/ // React SPA
├── angular-app/ // Angular SPA
├── vue-app/ // Vue.js SPA
└── shared-components/ // Shared UI Components
2.2 Micro-Frontends Architecture
// ASP.NET Core serving multiple frontends
public class FrontendRoutingMiddleware
{
private readonly RequestDelegate _next;
public FrontendRoutingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? "";
if (path.StartsWith("/react"))
{
// Serve React app
await ServeReactApp(context);
}
else if (path.StartsWith("/angular"))
{
// Serve Angular app
await ServeAngularApp(context);
}
else if (path.StartsWith("/vue"))
{
// Serve Vue app
await ServeVueApp(context);
}
else
{
await _next(context);
}
}
}
2.3 API Gateway Pattern
// Program.cs - API Gateway configuration
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
// appsettings.json
{
"ReverseProxy": {
"Routes": {
"react-app": {
"ClusterId": "react-cluster",
"Match": { "Path": "/react/{**catch-all}" }
},
"angular-app": {
"ClusterId": "angular-cluster",
"Match": { "Path": "/angular/{**catch-all}" }
},
"vue-app": {
"ClusterId": "vue-cluster",
"Match": { "Path": "/vue/{**catch-all}" }
}
},
"Clusters": {
"react-cluster": {
"Destinations": { "react-server": { "Address": "https://localhost:3000/" } }
},
"angular-cluster": {
"Destinations": { "angular-server": { "Address": "https://localhost:4200/" } }
},
"vue-cluster": {
"Destinations": { "vue-server": { "Address": "https://localhost:8080/" } }
}
}
}
}
3. Project Setup & Configuration
3.1 ASP.NET Core Backend Setup
// Program.cs - Modern minimal API setup
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Database Context
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// CORS for multiple frontends
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllFrontends", policy =>
{
policy.WithOrigins(
"http://localhost:3000", // React
"http://localhost:4200", // Angular
"http://localhost:8080") // Vue.js
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]))
};
});
// Swagger/OpenAPI
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ECommerce API", Version = "v1" });
// JWT Support in Swagger
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
});
var app = builder.Build();
// Configure pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("AllowAllFrontends");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
3.2 Shared Configuration Management
// appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=ECommerce;Trusted_Connection=true;TrustServerCertificate=true;"
},
"Jwt": {
"Secret": "your-super-secret-key-at-least-32-characters-long",
"Issuer": "ecommerce-api",
"Audience": "ecommerce-apps",
"ExpiryMinutes": 60
},
"Frontend": {
"ReactAppUrl": "http://localhost:3000",
"AngularAppUrl": "http://localhost:4200",
"VueAppUrl": "http://localhost:8080"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
// Frontend configuration service
public class FrontendConfigService
{
private readonly IConfiguration _configuration;
public FrontendConfigService(IConfiguration configuration)
{
_configuration = configuration;
}
public FrontendConfig GetConfig(string frontendType)
{
return frontendType.ToLower() switch
{
"react" => new FrontendConfig
{
ApiUrl = _configuration["ApiBaseUrl"],
AppUrl = _configuration["Frontend:ReactAppUrl"],
Features = new List<string> { "SSR", "PWA", "RealTime" }
},
"angular" => new FrontendConfig
{
ApiUrl = _configuration["ApiBaseUrl"],
AppUrl = _configuration["Frontend:AngularAppUrl"],
Features = new List<string> { "LazyLoading", "AOT", "Universal" }
},
"vue" => new FrontendConfig
{
ApiUrl = _configuration["ApiBaseUrl"],
AppUrl = _configuration["Frontend:VueAppUrl"],
Features = new List<string> { "CompositionAPI", "Vite", "Pinia" }
},
_ => throw new ArgumentException($"Unknown frontend type: {frontendType}")
};
}
}
public class FrontendConfig
{
public string ApiUrl { get; set; } = string.Empty;
public string AppUrl { get; set; } = string.Empty;
public List<string> Features { get; set; } = new();
}
4. React Integration Deep Dive
4.1 React Project Setup with TypeScript
// package.json for React + ASP.NET Core integration
{
"name": "ecommerce-react-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"serve": "npm run build && aspnetcore-https && dotnet run"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"axios": "^1.4.0",
"react-router-dom": "^6.14.1",
"zustand": "^4.3.9",
"react-hook-form": "^7.45.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}
4.2 API Service Layer
// src/services/apiClient.ts
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
class ApiClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
// Request interceptor
this.client.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('authToken');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
this.client.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
public async get<T>(url: string, params?: any): Promise<T> {
const response = await this.client.get<T>(url, { params });
return response.data;
}
public async post<T>(url: string, data?: any): Promise<T> {
const response = await this.client.post<T>(url, data);
return response.data;
}
public async put<T>(url: string, data?: any): Promise<T> {
const response = await this.client.put<T>(url, data);
return response.data;
}
public async delete<T>(url: string): Promise<T> {
const response = await this.client.delete<T>(url);
return response.data;
}
}
// Create API client instance
export const apiClient = new ApiClient(import.meta.env.VITE_API_BASE_URL);
4.3 React Hooks for API Integration
// src/hooks/useApi.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../services/apiClient';
// Product types
export interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
imageUrl: string;
stock: number;
createdAt: string;
}
export interface CreateProductRequest {
name: string;
description: string;
price: number;
category: string;
imageUrl: string;
stock: number;
}
// Product API hooks
export const useProducts = (category?: string) => {
return useQuery({
queryKey: ['products', category],
queryFn: () =>
apiClient.get<Product[]>('/api/products', { category }),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
export const useProduct = (id: number) => {
return useQuery({
queryKey: ['products', id],
queryFn: () => apiClient.get<Product>(`/api/products/${id}`),
enabled: !!id,
});
};
export const useCreateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (product: CreateProductRequest) =>
apiClient.post<Product>('/api/products', product),
onSuccess: () => {
// Invalidate and refetch products query
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};
export const useUpdateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, ...product }: Partial<Product> & { id: number }) =>
apiClient.put<Product>(`/api/products/${id}`, product),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['products'] });
queryClient.invalidateQueries({ queryKey: ['products', variables.id] });
},
});
};
export const useDeleteProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) =>
apiClient.delete(`/api/products/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};
4.4 React Components with ASP.NET Core Integration
// src/components/ProductList.tsx
import React from 'react';
import { useProducts, useDeleteProduct } from '../hooks/useApi';
import { ProductCard } from './ProductCard';
import { LoadingSpinner } from './LoadingSpinner';
import { ErrorMessage } from './ErrorMessage';
interface ProductListProps {
category?: string;
onProductSelect?: (product: Product) => void;
}
export const ProductList: React.FC<ProductListProps> = ({
category,
onProductSelect
}) => {
const { data: products, isLoading, error } = useProducts(category);
const deleteProductMutation = useDeleteProduct();
const handleDelete = async (productId: number) => {
if (window.confirm('Are you sure you want to delete this product?')) {
try {
await deleteProductMutation.mutateAsync(productId);
} catch (error) {
console.error('Failed to delete product:', error);
}
}
};
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message="Failed to load products" />;
return (
<div className="product-grid">
{products?.map((product) => (
<ProductCard
key={product.id}
product={product}
onSelect={onProductSelect}
onDelete={handleDelete}
canDelete={true}
/>
))}
</div>
);
};
4.5 Real-time Features with SignalR
// src/services/signalRService.ts
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
class SignalRService {
private connection: HubConnection | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
async connect(hubUrl: string): Promise<void> {
try {
this.connection = new HubConnectionBuilder()
.withUrl(hubUrl)
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(LogLevel.Information)
.build();
this.setupEventHandlers();
await this.connection.start();
console.log('SignalR Connected');
this.reconnectAttempts = 0;
} catch (error) {
console.error('SignalR Connection Failed:', error);
this.handleReconnection();
}
}
private setupEventHandlers(): void {
if (!this.connection) return;
this.connection.onclose(() => {
console.log('SignalR Connection Closed');
this.handleReconnection();
});
this.connection.onreconnecting(() => {
console.log('SignalR Reconnecting...');
});
this.connection.onreconnected(() => {
console.log('SignalR Reconnected');
this.reconnectAttempts = 0;
});
}
private handleReconnection(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect(this.connection?.connection.baseUrl || '');
}, Math.min(1000 * this.reconnectAttempts, 30000));
}
}
on<T>(methodName: string, callback: (data: T) => void): void {
this.connection?.on(methodName, callback);
}
async invoke<T>(methodName: string, ...args: any[]): Promise<T> {
if (!this.connection) {
throw new Error('SignalR connection not established');
}
return await this.connection.invoke<T>(methodName, ...args);
}
async disconnect(): Promise<void> {
if (this.connection) {
await this.connection.stop();
this.connection = null;
}
}
}
export const signalRService = new SignalRService();
5. Angular Enterprise Integration
5.1 Angular Project Structure
// angular.json - Enterprise configuration
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ecommerce-angular": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ecommerce-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/web.config"
],
"styles": [
"src/styles.scss",
"node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
],
"scripts": [],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all",
"optimization": true,
"sourceMap": false
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "ecommerce-angular:build:production"
}
}
}
}
}
}
}
5.2 Angular Services for ASP.NET Core Integration
// src/app/core/services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
export interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
totalCount?: number;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = environment.apiUrl;
constructor(private http: HttpClient) { }
get<T>(endpoint: string, params?: any): Observable<T> {
let httpParams = new HttpParams();
if (params) {
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined) {
httpParams = httpParams.set(key, params[key].toString());
}
});
}
return this.http.get<ApiResponse<T>>(`${this.baseUrl}${endpoint}`, { params: httpParams })
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<ApiResponse<T>>(`${this.baseUrl}${endpoint}`, data)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
put<T>(endpoint: string, data: any): Observable<T> {
return this.http.put<ApiResponse<T>>(`${this.baseUrl}${endpoint}`, data)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
delete<T>(endpoint: string): Observable<T> {
return this.http.delete<ApiResponse<T>>(`${this.baseUrl}${endpoint}`)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
private handleError(error: any) {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
5.3 Angular State Management with NgRx
// src/app/store/product/product.actions.ts
import { createAction, props } from '@ngrx/store';
import { Product } from '../../core/models/product.model';
export const loadProducts = createAction(
'[Product] Load Products',
props<{ category?: string }>()
);
export const loadProductsSuccess = createAction(
'[Product] Load Products Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
'[Product] Load Products Failure',
props<{ error: string }>()
);
export const createProduct = createAction(
'[Product] Create Product',
props<{ product: Omit<Product, 'id'> }>()
);
export const createProductSuccess = createAction(
'[Product] Create Product Success',
props<{ product: Product }>()
);
export const createProductFailure = createAction(
'[Product] Create Product Failure',
props<{ error: string }>()
);
// src/app/store/product/product.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { ProductService } from '../../core/services/product.service';
import * as ProductActions from './product.actions';
@Injectable()
export class ProductEffects {
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.loadProducts),
mergeMap((action) =>
this.productService.getProducts(action.category).pipe(
map(products => ProductActions.loadProductsSuccess({ products })),
catchError(error => of(ProductActions.loadProductsFailure({ error: error.message })))
)
)
)
);
createProduct$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.createProduct),
mergeMap((action) =>
this.productService.createProduct(action.product).pipe(
map(product => ProductActions.createProductSuccess({ product })),
catchError(error => of(ProductActions.createProductFailure({ error: error.message })))
)
)
)
);
constructor(
private actions$: Actions,
private productService: ProductService
) {}
}
5.4 Angular Authentication Interceptor
// src/app/core/interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getAccessToken();
if (token) {
request = this.addToken(request, token);
}
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
} else {
return throwError(() => error);
}
})
);
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addToken(request, token.accessToken));
}),
catchError((error) => {
this.isRefreshing = false;
this.authService.logout();
return throwError(() => error);
})
);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(token => {
return next.handle(this.addToken(request, token));
})
);
}
}
}
6. Vue.js Lightweight Integration
6.1 Vue 3 Composition API Integration
// src/composables/useApi.ts
import { ref, reactive } from 'vue';
import type { Ref } from 'vue';
import { apiClient } from '../services/apiClient';
interface ApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useApi<T>() {
const state = reactive<ApiState<T>>({
data: null,
loading: false,
error: null
});
const execute = async (apiCall: () => Promise<T>) => {
state.loading = true;
state.error = null;
try {
state.data = await apiCall();
} catch (error) {
state.error = error instanceof Error ? error.message : 'An error occurred';
console.error('API Error:', error);
} finally {
state.loading = false;
}
};
return {
state,
execute
};
}
// Product-specific composable
export function useProducts() {
const { state, execute } = useApi<Product[]>();
const fetchProducts = async (category?: string) => {
await execute(() =>
apiClient.get<Product[]>('/api/products', { category })
);
};
const createProduct = async (product: CreateProductRequest) => {
await execute(() =>
apiClient.post<Product>('/api/products', product)
);
};
return {
products: state,
fetchProducts,
createProduct
};
}
6.2 Vuex/Pinia Store Integration
// stores/products.ts - Pinia Store
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { apiClient } from '../services/apiClient';
import type { Product, CreateProductRequest } from '../types';
export const useProductStore = defineStore('products', () => {
// State
const products = ref<Product[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const selectedCategory = ref<string>('');
// Getters
const featuredProducts = computed(() =>
products.value.filter(product => product.price > 100)
);
const productsByCategory = computed(() =>
selectedCategory.value
? products.value.filter(product => product.category === selectedCategory.value)
: products.value
);
const totalValue = computed(() =>
products.value.reduce((total, product) => total + product.price, 0)
);
// Actions
const fetchProducts = async (category?: string) => {
loading.value = true;
error.value = null;
try {
const response = await apiClient.get<Product[]>('/api/products', { category });
products.value = response;
selectedCategory.value = category || '';
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch products';
console.error('Failed to fetch products:', err);
} finally {
loading.value = false;
}
};
const createProduct = async (productData: CreateProductRequest) => {
try {
const newProduct = await apiClient.post<Product>('/api/products', productData);
products.value.push(newProduct);
return newProduct;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to create product';
throw err;
}
};
const updateProduct = async (productId: number, updates: Partial<Product>) => {
try {
const updatedProduct = await apiClient.put<Product>(
`/api/products/${productId}`,
updates
);
const index = products.value.findIndex(p => p.id === productId);
if (index !== -1) {
products.value[index] = updatedProduct;
}
return updatedProduct;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to update product';
throw err;
}
};
const deleteProduct = async (productId: number) => {
try {
await apiClient.delete(`/api/products/${productId}`);
products.value = products.value.filter(p => p.id !== productId);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to delete product';
throw err;
}
};
return {
// State
products,
loading,
error,
selectedCategory,
// Getters
featuredProducts,
productsByCategory,
totalValue,
// Actions
fetchProducts,
createProduct,
updateProduct,
deleteProduct
};
});
6.3 Vue Components with ASP.NET Core Backend
<!-- src/components/ProductManagement.vue -->
<template>
<div class="product-management">
<div class="header">
<h2>Product Management</h2>
<button @click="showCreateForm = true" class="btn-primary">
Add New Product
</button>
</div>
<!-- Filters -->
<div class="filters">
<select v-model="selectedCategory" @change="handleCategoryChange">
<option value="">All Categories</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
<input
v-model="searchQuery"
type="text"
placeholder="Search products..."
@input="handleSearch"
/>
</div>
<!-- Loading State -->
<div v-if="productStore.loading" class="loading">
<LoadingSpinner />
</div>
<!-- Error State -->
<div v-else-if="productStore.error" class="error">
{{ productStore.error }}
<button @click="loadProducts" class="btn-secondary">Retry</button>
</div>
<!-- Products Grid -->
<div v-else class="products-grid">
<ProductCard
v-for="product in filteredProducts"
:key="product.id"
:product="product"
@edit="handleEdit"
@delete="handleDelete"
/>
</div>
<!-- Empty State -->
<div v-if="!productStore.loading && filteredProducts.length === 0" class="empty-state">
<p>No products found.</p>
</div>
<!-- Create/Edit Modal -->
<ProductFormModal
v-if="showCreateForm || editingProduct"
:product="editingProduct"
@save="handleSave"
@cancel="handleCancel"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useProductStore } from '../stores/products';
import ProductCard from './ProductCard.vue';
import ProductFormModal from './ProductFormModal.vue';
import LoadingSpinner from './LoadingSpinner.vue';
import type { Product, CreateProductRequest } from '../types';
const productStore = useProductStore();
const selectedCategory = ref('');
const searchQuery = ref('');
const showCreateForm = ref(false);
const editingProduct = ref<Product | null>(null);
// Computed properties
const categories = computed(() =>
Array.from(new Set(productStore.products.map(p => p.category)))
);
const filteredProducts = computed(() => {
let products = productStore.productsByCategory;
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
products = products.filter(product =>
product.name.toLowerCase().includes(query) ||
product.description.toLowerCase().includes(query)
);
}
return products;
});
// Lifecycle
onMounted(() => {
loadProducts();
});
// Methods
const loadProducts = () => {
productStore.fetchProducts(selectedCategory.value);
};
const handleCategoryChange = () => {
loadProducts();
};
const handleSearch = () => {
// Search is handled by computed property
};
const handleEdit = (product: Product) => {
editingProduct.value = { ...product };
};
const handleDelete = async (productId: number) => {
if (confirm('Are you sure you want to delete this product?')) {
try {
await productStore.deleteProduct(productId);
} catch (error) {
console.error('Failed to delete product:', error);
}
}
};
const handleSave = async (productData: CreateProductRequest | Product) => {
try {
if (editingProduct.value) {
// Update existing product
await productStore.updateProduct(
(productData as Product).id,
productData
);
} else {
// Create new product
await productStore.createProduct(productData as CreateProductRequest);
}
// Reset form state
showCreateForm.value = false;
editingProduct.value = null;
} catch (error) {
console.error('Failed to save product:', error);
}
};
const handleCancel = () => {
showCreateForm.value = false;
editingProduct.value = null;
};
</script>
<style scoped>
.product-management {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.filters select,
.filters input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.loading, .error {
text-align: center;
padding: 40px;
}
.error {
color: #d32f2f;
}
</style>
7. Real-World E-Commerce Example
7.1 ASP.NET Core Backend API Controllers
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace ECommerce.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<ProductsController> _logger;
public ProductsController(
ApplicationDbContext context,
ILogger<ProductsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<ApiResponse<PagedResult<ProductDto>>>> GetProducts(
[FromQuery] ProductQueryParameters parameters)
{
try
{
var query = _context.Products.AsQueryable();
// Filter by category
if (!string.IsNullOrEmpty(parameters.Category))
{
query = query.Where(p => p.Category == parameters.Category);
}
// Search by name or description
if (!string.IsNullOrEmpty(parameters.SearchTerm))
{
query = query.Where(p =>
p.Name.Contains(parameters.SearchTerm) ||
p.Description.Contains(parameters.SearchTerm));
}
// Price range filter
if (parameters.MinPrice.HasValue)
{
query = query.Where(p => p.Price >= parameters.MinPrice.Value);
}
if (parameters.MaxPrice.HasValue)
{
query = query.Where(p => p.Price <= parameters.MaxPrice.Value);
}
// Get total count for pagination
var totalCount = await query.CountAsync();
// Apply sorting
query = parameters.SortBy?.ToLower() switch
{
"price" => parameters.SortDescending ?? false
? query.OrderByDescending(p => p.Price)
: query.OrderBy(p => p.Price),
"name" => parameters.SortDescending ?? false
? query.OrderByDescending(p => p.Name)
: query.OrderBy(p => p.Name),
"date" => parameters.SortDescending ?? false
? query.OrderByDescending(p => p.CreatedAt)
: query.OrderBy(p => p.CreatedAt),
_ => query.OrderByDescending(p => p.CreatedAt)
};
// Apply pagination
var products = await query
.Skip((parameters.Page - 1) * parameters.PageSize)
.Take(parameters.PageSize)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Category = p.Category,
ImageUrl = p.ImageUrl,
Stock = p.Stock,
CreatedAt = p.CreatedAt
})
.ToListAsync();
var result = new PagedResult<ProductDto>
{
Items = products,
TotalCount = totalCount,
Page = parameters.Page,
PageSize = parameters.PageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)parameters.PageSize)
};
return Ok(ApiResponse<PagedResult<ProductDto>>.Success(result));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving products");
return StatusCode(500, ApiResponse<object>.Failure("An error occurred while retrieving products"));
}
}
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<ProductDto>>> GetProduct(int id)
{
try
{
var product = await _context.Products
.Where(p => p.Id == id)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Category = p.Category,
ImageUrl = p.ImageUrl,
Stock = p.Stock,
CreatedAt = p.CreatedAt
})
.FirstOrDefaultAsync();
if (product == null)
{
return NotFound(ApiResponse<object>.Failure("Product not found"));
}
return Ok(ApiResponse<ProductDto>.Success(product));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving product with ID {ProductId}", id);
return StatusCode(500, ApiResponse<object>.Failure("An error occurred while retrieving the product"));
}
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ApiResponse<ProductDto>>> CreateProduct(CreateProductRequest request)
{
try
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price,
Category = request.Category,
ImageUrl = request.ImageUrl,
Stock = request.Stock,
CreatedAt = DateTime.UtcNow
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
var productDto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Category = product.Category,
ImageUrl = product.ImageUrl,
Stock = product.Stock,
CreatedAt = product.CreatedAt
};
return CreatedAtAction(
nameof(GetProduct),
new { id = product.Id },
ApiResponse<ProductDto>.Success(productDto));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating product");
return StatusCode(500, ApiResponse<object>.Failure("An error occurred while creating the product"));
}
}
[HttpPut("{id}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ApiResponse<ProductDto>>> UpdateProduct(
int id,
UpdateProductRequest request)
{
try
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound(ApiResponse<object>.Failure("Product not found"));
}
product.Name = request.Name ?? product.Name;
product.Description = request.Description ?? product.Description;
product.Price = request.Price ?? product.Price;
product.Category = request.Category ?? product.Category;
product.ImageUrl = request.ImageUrl ?? product.ImageUrl;
product.Stock = request.Stock ?? product.Stock;
await _context.SaveChangesAsync();
var productDto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Category = product.Category,
ImageUrl = product.ImageUrl,
Stock = product.Stock,
CreatedAt = product.CreatedAt
};
return Ok(ApiResponse<ProductDto>.Success(productDto));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating product with ID {ProductId}", id);
return StatusCode(500, ApiResponse<object>.Failure("An error occurred while updating the product"));
}
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ApiResponse<object>>> DeleteProduct(int id)
{
try
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound(ApiResponse<object>.Failure("Product not found"));
}
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return Ok(ApiResponse<object>.Success(null, "Product deleted successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting product with ID {ProductId}", id);
return StatusCode(500, ApiResponse<object>.Failure("An error occurred while deleting the product"));
}
}
}
public class ProductQueryParameters
{
[Range(1, int.MaxValue)]
public int Page { get; set; } = 1;
[Range(1, 100)]
public int PageSize { get; set; } = 10;
public string? Category { get; set; }
public string? SearchTerm { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public string? SortBy { get; set; }
public bool? SortDescending { get; set; }
}
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
}
public class ApiResponse<T>
{
public bool Success { get; set; }
public string? Message { get; set; }
public T? Data { get; set; }
public static ApiResponse<T> Success(T data, string? message = null)
{
return new ApiResponse<T> { Success = true, Data = data, Message = message };
}
public static ApiResponse<T> Failure(string message)
{
return new ApiResponse<T> { Success = false, Message = message };
}
}
}
7.2 Real-time Inventory Management with SignalR
// Hubs/InventoryHub.cs
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace ECommerce.API.Hubs
{
public class InventoryHub : Hub
{
private readonly ApplicationDbContext _context;
private static readonly Dictionary<string, string> _userGroups = new();
public InventoryHub(ApplicationDbContext context)
{
_context = context;
}
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
var userId = httpContext?.Request.Query["userId"].ToString();
if (!string.IsNullOrEmpty(userId))
{
_userGroups[Context.ConnectionId] = userId;
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}");
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (_userGroups.ContainsKey(Context.ConnectionId))
{
var userId = _userGroups[Context.ConnectionId];
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}");
_userGroups.Remove(Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception);
}
public async Task SubscribeToProduct(int productId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"product-{productId}");
}
public async Task UnsubscribeFromProduct(int productId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"product-{productId}");
}
public async Task UpdateStock(int productId, int newStock)
{
var product = await _context.Products.FindAsync(productId);
if (product != null)
{
product.Stock = newStock;
await _context.SaveChangesAsync();
// Notify all subscribers
await Clients.Group($"product-{productId}")
.SendAsync("StockUpdated", productId, newStock);
}
}
}
}
// Services/InventoryNotificationService.cs
using Microsoft.AspNetCore.SignalR;
namespace ECommerce.API.Services
{
public interface IInventoryNotificationService
{
Task NotifyStockUpdate(int productId, int newStock);
Task NotifyLowStock(int productId, int currentStock);
Task NotifyProductUpdate(int productId);
}
public class InventoryNotificationService : IInventoryNotificationService
{
private readonly IHubContext<InventoryHub> _hubContext;
private readonly ILogger<InventoryNotificationService> _logger;
public InventoryNotificationService(
IHubContext<InventoryHub> hubContext,
ILogger<InventoryNotificationService> logger)
{
_hubContext = hubContext;
_logger = logger;
}
public async Task NotifyStockUpdate(int productId, int newStock)
{
try
{
await _hubContext.Clients.Group($"product-{productId}")
.SendAsync("StockUpdated", productId, newStock);
_logger.LogInformation("Stock update notified for product {ProductId}", productId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error notifying stock update for product {ProductId}", productId);
}
}
public async Task NotifyLowStock(int productId, int currentStock)
{
try
{
await _hubContext.Clients.Group("admin-users")
.SendAsync("LowStockAlert", productId, currentStock);
_logger.LogWarning("Low stock alert sent for product {ProductId}", productId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending low stock alert for product {ProductId}", productId);
}
}
public async Task NotifyProductUpdate(int productId)
{
try
{
await _hubContext.Clients.Group($"product-{productId}")
.SendAsync("ProductUpdated", productId);
_logger.LogInformation("Product update notified for product {ProductId}", productId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error notifying product update for product {ProductId}", productId);
}
}
}
}
8. Performance Optimization
8.1 Caching Strategies
// Services/CachingService.cs
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace ECommerce.API.Services
{
public interface ICachingService
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
Task RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
}
public class CachingService : ICachingService
{
private readonly IDistributedCache _cache;
private readonly ILogger<CachingService> _logger;
public CachingService(IDistributedCache cache, ILogger<CachingService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T?> GetAsync<T>(string key)
{
try
{
var cachedData = await _cache.GetStringAsync(key);
if (cachedData == null)
{
_logger.LogDebug("Cache miss for key: {CacheKey}", key);
return default;
}
_logger.LogDebug("Cache hit for key: {CacheKey}", key);
return JsonSerializer.Deserialize<T>(cachedData);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving cache for key: {CacheKey}", key);
return default;
}
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
try
{
var options = new DistributedCacheEntryOptions();
if (expiration.HasValue)
{
options.SetAbsoluteExpiration(expiration.Value);
}
else
{
// Default expiration: 5 minutes
options.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
}
var serializedData = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serializedData, options);
_logger.LogDebug("Cache set for key: {CacheKey}", key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting cache for key: {CacheKey}", key);
}
}
public async Task RemoveAsync(string key)
{
try
{
await _cache.RemoveAsync(key);
_logger.LogDebug("Cache removed for key: {CacheKey}", key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing cache for key: {CacheKey}", key);
}
}
public async Task<bool> ExistsAsync(string key)
{
try
{
var cachedData = await _cache.GetStringAsync(key);
return cachedData != null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking cache existence for key: {CacheKey}", key);
return false;
}
}
}
}
// Cached Repository Pattern
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _decorated;
private readonly ICachingService _cachingService;
private readonly ILogger<CachedProductRepository> _logger;
public CachedProductRepository(
IProductRepository decorated,
ICachingService cachingService,
ILogger<CachedProductRepository> logger)
{
_decorated = decorated;
_cachingService = cachingService;
_logger = logger;
}
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product-{id}";
var cachedProduct = await _cachingService.GetAsync<Product>(cacheKey);
if (cachedProduct != null)
{
return cachedProduct;
}
var product = await _decorated.GetByIdAsync(id);
if (product != null)
{
await _cachingService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
}
return product;
}
public async Task<PagedResult<Product>> GetProductsAsync(ProductQueryParameters parameters)
{
// Create cache key from parameters
var cacheKey = $"products-{JsonSerializer.Serialize(parameters)}";
var cachedResult = await _cachingService.GetAsync<PagedResult<Product>>(cacheKey);
if (cachedResult != null)
{
return cachedResult;
}
var result = await _decorated.GetProductsAsync(parameters);
await _cachingService.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
}
public async Task<Product> CreateAsync(Product product)
{
var result = await _decorated.CreateAsync(product);
// Invalidate relevant caches
await _cachingService.RemoveAsync("products-");
await _cachingService.RemoveAsync($"product-{result.Id}");
return result;
}
public async Task UpdateAsync(Product product)
{
await _decorated.UpdateAsync(product);
// Invalidate relevant caches
await _cachingService.RemoveAsync("products-");
await _cachingService.RemoveAsync($"product-{product.Id}");
}
public async Task DeleteAsync(int id)
{
await _decorated.DeleteAsync(id);
// Invalidate relevant caches
await _cachingService.RemoveAsync("products-");
await _cachingService.RemoveAsync($"product-{id}");
}
}
8.2 Frontend Performance Optimization
// React Performance Optimization with React.memo and useMemo
import React, { memo, useMemo, useCallback } from 'react';
interface ProductCardProps {
product: Product;
onSelect: (product: Product) => void;
onDelete: (productId: number) => void;
canDelete: boolean;
}
export const ProductCard = memo<ProductCardProps>(({
product,
onSelect,
onDelete,
canDelete
}) => {
const handleSelect = useCallback(() => {
onSelect(product);
}, [onSelect, product]);
const handleDelete = useCallback(() => {
onDelete(product.id);
}, [onDelete, product.id]);
const formattedPrice = useMemo(() => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(product.price);
}, [product.price]);
const stockStatus = useMemo(() => {
if (product.stock === 0) return 'Out of Stock';
if (product.stock < 10) return 'Low Stock';
return 'In Stock';
}, [product.stock]);
return (
<div className="product-card">
<img
src={product.imageUrl}
alt={product.name}
loading="lazy"
/>
<div className="product-info">
<h3>{product.name}</h3>
<p className="description">{product.description}</p>
<div className="price">{formattedPrice}</div>
<div className={`stock ${stockStatus.toLowerCase().replace(' ', '-')}`}>
{stockStatus}
</div>
<div className="actions">
<button onClick={handleSelect} className="btn-primary">
View Details
</button>
{canDelete && (
<button onClick={handleDelete} className="btn-danger">
Delete
</button>
)}
</div>
</div>
</div>
);
});
ProductCard.displayName = 'ProductCard';
// Lazy loading with React Suspense
const ProductManagement = lazy(() =>
import('./ProductManagement').then(module => ({
default: module.ProductManagement
}))
);
const AnalyticsDashboard = lazy(() =>
import('./AnalyticsDashboard').then(module => ({
default: module.AnalyticsDashboard
}))
);
export const App: React.FC = () => {
return (
<Router>
<div className="app">
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/products/*" element={<ProductManagement />} />
<Route path="/analytics" element={<AnalyticsDashboard />} />
</Routes>
</Suspense>
</div>
</Router>
);
};
9. Security Considerations
9.1 JWT Authentication & Authorization
// Services/AuthService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace ECommerce.API.Services
{
public interface IAuthService
{
Task<AuthResult> LoginAsync(LoginRequest request);
Task<AuthResult> RegisterAsync(RegisterRequest request);
Task<AuthResult> RefreshTokenAsync(string token, string refreshToken);
Task<bool> RevokeTokenAsync(string userId);
}
public class AuthService : IAuthService
{
private readonly ApplicationDbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<AuthService> _logger;
public AuthService(
ApplicationDbContext context,
IConfiguration configuration,
ILogger<AuthService> logger)
{
_context = context;
_configuration = configuration;
_logger = logger;
}
public async Task<AuthResult> LoginAsync(LoginRequest request)
{
try
{
var user = await _context.Users
.Include(u => u.Roles)
.FirstOrDefaultAsync(u => u.Email == request.Email);
if (user == null || !VerifyPassword(request.Password, user.PasswordHash))
{
return AuthResult.Failure("Invalid email or password");
}
var token = GenerateJwtToken(user);
var refreshToken = GenerateRefreshToken();
user.RefreshToken = refreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
await _context.SaveChangesAsync();
return AuthResult.Success(token, refreshToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during login for email: {Email}", request.Email);
return AuthResult.Failure("An error occurred during login");
}
}
public async Task<AuthResult> RegisterAsync(RegisterRequest request)
{
try
{
if (await _context.Users.AnyAsync(u => u.Email == request.Email))
{
return AuthResult.Failure("Email already exists");
}
var user = new User
{
Email = request.Email,
PasswordHash = HashPassword(request.Password),
FirstName = request.FirstName,
LastName = request.LastName,
CreatedAt = DateTime.UtcNow
};
// Assign default role
var defaultRole = await _context.Roles.FirstOrDefaultAsync(r => r.Name == "User");
if (defaultRole != null)
{
user.Roles.Add(defaultRole);
}
_context.Users.Add(user);
await _context.SaveChangesAsync();
var token = GenerateJwtToken(user);
var refreshToken = GenerateRefreshToken();
user.RefreshToken = refreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
await _context.SaveChangesAsync();
return AuthResult.Success(token, refreshToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during registration for email: {Email}", request.Email);
return AuthResult.Failure("An error occurred during registration");
}
}
public async Task<AuthResult> RefreshTokenAsync(string token, string refreshToken)
{
try
{
var principal = GetPrincipalFromExpiredToken(token);
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _context.Users
.Include(u => u.Roles)
.FirstOrDefaultAsync(u => u.Id == int.Parse(userId));
if (user == null || user.RefreshToken != refreshToken ||
user.RefreshTokenExpiry <= DateTime.UtcNow)
{
return AuthResult.Failure("Invalid refresh token");
}
var newToken = GenerateJwtToken(user);
var newRefreshToken = GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
await _context.SaveChangesAsync();
return AuthResult.Success(newToken, newRefreshToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during token refresh");
return AuthResult.Failure("An error occurred during token refresh");
}
}
public async Task<bool> RevokeTokenAsync(string userId)
{
try
{
var user = await _context.Users.FindAsync(int.Parse(userId));
if (user == null) return false;
user.RefreshToken = null;
user.RefreshTokenExpiry = null;
await _context.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error revoking token for user: {UserId}", userId);
return false;
}
}
private string GenerateJwtToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
// Add roles
claims.AddRange(user.Roles.Select(role =>
new Claim(ClaimTypes.Role, role.Name)));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(
double.Parse(_configuration["Jwt:ExpiryMinutes"] ?? "60")),
Issuer = _configuration["Jwt:Issuer"],
Audience = _configuration["Jwt:Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = false
};
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out _);
return principal;
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
private string HashPassword(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}
private bool VerifyPassword(string password, string passwordHash)
{
return BCrypt.Net.BCrypt.Verify(password, passwordHash);
}
}
public class AuthResult
{
public bool Success { get; set; }
public string? Token { get; set; }
public string? RefreshToken { get; set; }
public string? Error { get; set; }
public static AuthResult Success(string token, string refreshToken)
{
return new AuthResult
{
Success = true,
Token = token,
RefreshToken = refreshToken
};
}
public static AuthResult Failure(string error)
{
return new AuthResult { Success = false, Error = error };
}
}
}
9.2 Frontend Security Implementation
// React Security Hook
import { useState, useEffect, useCallback } from 'react';
interface User {
id: number;
email: string;
firstName: string;
lastName: string;
roles: string[];
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
export const useAuth = () => {
const [authState, setAuthState] = useState<AuthState>({
user: null,
isAuthenticated: false,
isLoading: true,
error: null
});
const login = useCallback(async (email: string, password: string) => {
try {
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
const response = await apiClient.post<AuthResponse>('/api/auth/login', {
email,
password
});
const { token, refreshToken, user } = response;
// Store tokens securely
localStorage.setItem('authToken', token);
localStorage.setItem('refreshToken', refreshToken);
setAuthState({
user,
isAuthenticated: true,
isLoading: false,
error: null
});
} catch (error) {
setAuthState(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Login failed'
}));
}
}, []);
const logout = useCallback(() => {
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
setAuthState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null
});
}, []);
const refreshToken = useCallback(async () => {
try {
const token = localStorage.getItem('authToken');
const refreshToken = localStorage.getItem('refreshToken');
if (!token || !refreshToken) {
throw new Error('No tokens available');
}
const response = await apiClient.post<AuthResponse>('/api/auth/refresh', {
token,
refreshToken
});
localStorage.setItem('authToken', response.token);
localStorage.setItem('refreshToken', response.refreshToken);
setAuthState(prev => ({
...prev,
user: response.user,
isAuthenticated: true
}));
return response.token;
} catch (error) {
logout();
throw error;
}
}, [logout]);
useEffect(() => {
const initializeAuth = async () => {
const token = localStorage.getItem('authToken');
if (!token) {
setAuthState(prev => ({ ...prev, isLoading: false }));
return;
}
try {
// Verify token validity by fetching user profile
const user = await apiClient.get<User>('/api/auth/profile');
setAuthState({
user,
isAuthenticated: true,
isLoading: false,
error: null
});
} catch (error) {
// Token is invalid, clear storage
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
setAuthState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null
});
}
};
initializeAuth();
}, []);
return {
...authState,
login,
logout,
refreshToken
};
};
// Protected Route Component
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRoles?: string[];
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requiredRoles = []
}) => {
const { isAuthenticated, user, isLoading } = useAuth();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (requiredRoles.length > 0 && user) {
const hasRequiredRole = requiredRoles.some(role =>
user.roles.includes(role)
);
if (!hasRequiredRole) {
return <Navigate to="/unauthorized" replace />;
}
}
return <>{children}</>;
};
10. Deployment Strategies
10.1 Docker Configuration
# Backend Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["ECommerce.API/ECommerce.API.csproj", "ECommerce.API/"]
COPY ["ECommerce.Application/ECommerce.Application.csproj", "ECommerce.Application/"]
COPY ["ECommerce.Domain/ECommerce.Domain.csproj", "ECommerce.Domain/"]
COPY ["ECommerce.Infrastructure/ECommerce.Infrastructure.csproj", "ECommerce.Infrastructure/"]
COPY ["ECommerce.Shared/ECommerce.Shared.csproj", "ECommerce.Shared/"]
RUN dotnet restore "ECommerce.API/ECommerce.API.csproj"
COPY . .
WORKDIR "/src/ECommerce.API"
RUN dotnet build "ECommerce.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ECommerce.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ECommerce.API.dll"]
# Frontend Dockerfile (React example)
FROM node:18-alpine AS frontend-build
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=frontend-build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
10.2 Docker Compose for Full Stack
# docker-compose.yml
version: '3.8'
services:
# Database
postgres:
image: postgres:15
environment:
POSTGRES_DB: ecommerce
POSTGRES_USER: admin
POSTGRES_PASSWORD: securepassword
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- ecommerce-network
# Redis Cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- ecommerce-network
# Backend API
api:
build:
context: .
dockerfile: Dockerfile
environment:
- ConnectionStrings__DefaultConnection=Host=postgres;Database=ecommerce;Username=admin;Password=securepassword
- Redis__ConnectionString=redis:6379
- Jwt__Secret=your-super-secret-key-at-least-32-characters-long
- ASPNETCORE_ENVIRONMENT=Production
ports:
- "5000:80"
depends_on:
- postgres
- redis
networks:
- ecommerce-network
# React Frontend
react-app:
build:
context: ./frontend/react-app
dockerfile: Dockerfile
environment:
- VITE_API_BASE_URL=http://localhost:5000/api
ports:
- "3000:80"
depends_on:
- api
networks:
- ecommerce-network
# Angular Frontend
angular-app:
build:
context: ./frontend/angular-app
dockerfile: Dockerfile
environment:
- API_BASE_URL=http://localhost:5000/api
ports:
- "4200:80"
depends_on:
- api
networks:
- ecommerce-network
# Vue.js Frontend
vue-app:
build:
context: ./frontend/vue-app
dockerfile: Dockerfile
environment:
- VITE_API_BASE_URL=http://localhost:5000/api
ports:
- "8080:80"
depends_on:
- api
networks:
- ecommerce-network
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- api
- react-app
- angular-app
- vue-app
networks:
- ecommerce-network
volumes:
postgres_data:
networks:
ecommerce-network:
driver: bridge
10.3 CI/CD Pipeline Configuration
# .github/workflows/deploy.yml
name: Deploy ECommerce Application
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Run tests
run: dotnet test --verbosity normal
- name: Publish code coverage
uses: codecov/codecov-action@v3
build-backend:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t ecommerce-api:${{ github.sha }} .
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push Docker image
run: |
docker tag ecommerce-api:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/ecommerce-api:latest
docker push ${{ secrets.DOCKER_USERNAME }}/ecommerce-api:latest
build-frontend:
runs-on: ubuntu-latest
needs: test
strategy:
matrix:
frontend: [react, angular, vue]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/${{ matrix.frontend }}-app/package-lock.json
- name: Install dependencies
run: npm ci
working-directory: frontend/${{ matrix.frontend }}-app
- name: Build application
run: npm run build
working-directory: frontend/${{ matrix.frontend }}-app
env:
VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}
- name: Build Docker image
run: docker build -t ecommerce-${{ matrix.frontend }}:${{ github.sha }} .
working-directory: frontend/${{ matrix.frontend }}-app
- name: Push Docker image
run: |
docker tag ecommerce-${{ matrix.frontend }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/ecommerce-${{ matrix.frontend }}:latest
docker push ${{ secrets.DOCKER_USERNAME }}/ecommerce-${{ matrix.frontend }}:latest
deploy:
runs-on: ubuntu-latest
needs: [build-backend, build-frontend]
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/ecommerce
docker-compose pull
docker-compose up -d
docker system prune -f
This comprehensive guide covers the complete integration of JavaScript frameworks with ASP.NET Core, providing real-world examples, best practices, and production-ready code patterns. The modular approach allows you to choose the integration strategy that best fits your project requirements while maintaining scalability, performance, and security.