Frontend Fusion: Mastering JavaScript Framework Integration with ASP.NET Core
Learn to seamlessly blend React, Angular, and Vue.js with ASP.NET Core. Build powerful hybrid applications with real-world examples and best practices.
ASPNETCore,JavaScriptFrameworks,ReactIntegration,AngularASP.NET,VueJS,FullStackDevelopment,WebArchitecture,FrontendBackend,SPAIntegration,HybridApps
 
Module Sequence: Frontend Fusion Mastery
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
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:
Independent development workflows
Technology flexibility
Improved performance
Better team specialization
Enhanced user experience
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/ssh-action@v0.1.7 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.
.png)
0 Comments
thanks for your comments!