Technology

C# Blazor 12-Factor App สร้าง Cloud-Native Web App ด้วย .NET 8

C# Blazor 12 Factor App | SiamCafe Blog
2026-05-16· อ. บอม — SiamCafe.net· 1,427 คำ

Blazor ????????? 12-Factor App ?????????????????????

Blazor ???????????? web framework ????????? Microsoft ????????????????????????????????? interactive web UI ???????????? C# ????????? JavaScript ?????????????????????????????? Blazor Server (render ???????????? server ???????????? SignalR) ????????? Blazor WebAssembly (render ???????????? client ?????? browser) ???????????????????????????????????????????????? ASP.NET Core ecosystem

12-Factor App ???????????? methodology ????????????????????????????????? software-as-a-service applications ??????????????? ??????????????????????????? Heroku ?????? 12 ????????????????????? Codebase (one codebase tracked in revision control), Dependencies (explicitly declare and isolate), Config (store config in environment), Backing Services (treat as attached resources), Build Release Run (strictly separate stages), Processes (execute as stateless processes), Port Binding (export services via port binding), Concurrency (scale out via process model), Disposability (maximize robustness with fast startup and graceful shutdown), Dev/Prod Parity (keep environments similar), Logs (treat logs as event streams), Admin Processes (run admin tasks as one-off processes)

??????????????? 12-Factor ???????????????????????? Blazor ??????????????? application scalable, maintainable, portable deploy ?????????????????? cloud provider (Azure, AWS, GCP) ?????????????????? container ???????????????

????????????????????? Blazor Project ????????? 12-Factor

??????????????? Blazor project structure

# === Blazor 12-Factor Project Setup ===

# 1. Create Blazor Server App
dotnet new blazorserver -o MyApp.Web --framework net8.0
cd MyApp.Web

# 2. Project Structure (12-Factor compliant)
# MyApp/
#   MyApp.Web/          ??? Blazor Server app
#   MyApp.Core/         ??? Business logic (shared)
#   MyApp.Infrastructure/ ??? Data access, external services
#   MyApp.Tests/        ??? Unit & integration tests
#   docker-compose.yml
#   Dockerfile

# 3. Create Solution
dotnet new sln -n MyApp
dotnet new classlib -o MyApp.Core --framework net8.0
dotnet new classlib -o MyApp.Infrastructure --framework net8.0
dotnet new xunit -o MyApp.Tests --framework net8.0

dotnet sln add MyApp.Web/MyApp.Web.csproj
dotnet sln add MyApp.Core/MyApp.Core.csproj
dotnet sln add MyApp.Infrastructure/MyApp.Infrastructure.csproj
dotnet sln add MyApp.Tests/MyApp.Tests.csproj

# 4. Add References
dotnet add MyApp.Web reference MyApp.Core MyApp.Infrastructure
dotnet add MyApp.Infrastructure reference MyApp.Core
dotnet add MyApp.Tests reference MyApp.Core MyApp.Infrastructure

# 5. Dockerfile (Factor V: Build, Release, Run)
cat > Dockerfile << 'EOF'
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.sln .
COPY MyApp.Web/*.csproj MyApp.Web/
COPY MyApp.Core/*.csproj MyApp.Core/
COPY MyApp.Infrastructure/*.csproj MyApp.Infrastructure/
RUN dotnet restore

COPY . .
RUN dotnet publish MyApp.Web -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.Web.dll"]
EOF

# 6. docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  web:
    build: .
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__Default=Host=db;Database=myapp;Username=app;Password=secret
      - Redis__ConnectionString=redis:6379
    depends_on:
      - db
      - redis
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
  redis:
    image: redis:7-alpine
volumes:
  pgdata:
EOF

echo "Blazor 12-Factor project created"

Configuration ????????? Environment Management

Factor III: Config ??? ?????????????????? configuration ???????????? environment

// === Factor III: Configuration Management ===
// Program.cs ??? Blazor Server Configuration

// File: Program.cs
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Factor III: Config from environment variables
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddEnvironmentVariables()  // Override with env vars
    .AddCommandLine(args);

// Strongly-typed configuration (Factor III)
builder.Services.Configure<DatabaseConfig>(
    builder.Configuration.GetSection("Database"));
builder.Services.Configure<CacheConfig>(
    builder.Configuration.GetSection("Cache"));
builder.Services.Configure<AuthConfig>(
    builder.Configuration.GetSection("Auth"));

// Factor IV: Backing Services as attached resources
var dbConnection = builder.Configuration.GetConnectionString("Default");
var redisConnection = builder.Configuration["Redis:ConnectionString"];

// Register services (Factor VI: Stateless processes)
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Factor XI: Logs as event streams
builder.Logging.ClearProviders();
builder.Logging.AddConsole();  // stdout
builder.Logging.AddJsonConsole(options =>
{
    options.IncludeScopes = true;
    options.TimestampFormat = "yyyy-MM-ddTHH:mm:ssZ";
});

var app = builder.Build();

// Factor VII: Port Binding
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

// Configuration classes
public class DatabaseConfig
{
    public string ConnectionString { get; set; } = "";
    public int MaxPoolSize { get; set; } = 20;
    public int CommandTimeout { get; set; } = 30;
}

public class CacheConfig
{
    public string ConnectionString { get; set; } = "";
    public int DefaultTtlMinutes { get; set; } = 60;
}

public class AuthConfig
{
    public string JwtSecret { get; set; } = "";
    public int TokenExpiryMinutes { get; set; } = 60;
    public string Issuer { get; set; } = "";
}

Dependency Injection ????????? Backing Services

Factor II ????????? IV: Dependencies ????????? Backing Services

// === Factor II: Dependencies & Factor IV: Backing Services ===

// File: MyApp.Core/Interfaces/IProductRepository.cs
namespace MyApp.Core.Interfaces;

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync();
    Task<Product?> GetByIdAsync(int id);
    Task<Product> CreateAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
    public string Category { get; set; } = "";
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

// File: MyApp.Infrastructure/Repositories/ProductRepository.cs
namespace MyApp.Infrastructure.Repositories;

using Microsoft.Extensions.Logging;
using MyApp.Core.Interfaces;
using System.Collections.Concurrent;

public class ProductRepository : IProductRepository
{
    private readonly ILogger<ProductRepository> _logger;
    private static readonly ConcurrentDictionary<int, Product> _store = new();
    private static int _nextId = 1;

    public ProductRepository(ILogger<ProductRepository> logger)
    {
        _logger = logger;
    }

    public Task<IEnumerable<Product>> GetAllAsync()
    {
        _logger.LogInformation("Getting all products, count: {Count}", _store.Count);
        return Task.FromResult<IEnumerable<Product>>(_store.Values.ToList());
    }

    public Task<Product?> GetByIdAsync(int id)
    {
        _store.TryGetValue(id, out var product);
        return Task.FromResult(product);
    }

    public Task<Product> CreateAsync(Product product)
    {
        product.Id = Interlocked.Increment(ref _nextId);
        product.CreatedAt = DateTime.UtcNow;
        product.UpdatedAt = DateTime.UtcNow;
        _store[product.Id] = product;
        _logger.LogInformation("Created product {Id}: {Name}", product.Id, product.Name);
        return Task.FromResult(product);
    }

    public Task UpdateAsync(Product product)
    {
        product.UpdatedAt = DateTime.UtcNow;
        _store[product.Id] = product;
        return Task.CompletedTask;
    }

    public Task DeleteAsync(int id)
    {
        _store.TryRemove(id, out _);
        return Task.CompletedTask;
    }
}

// File: MyApp.Infrastructure/DependencyInjection.cs
// Register all infrastructure services
namespace MyApp.Infrastructure;

using Microsoft.Extensions.DependencyInjection;
using MyApp.Core.Interfaces;
using MyApp.Infrastructure.Repositories;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services)
    {
        // Factor IV: Backing services as attached resources
        services.AddScoped<IProductRepository, ProductRepository>();
        return services;
    }
}

Build, Release, Run ????????? CI/CD

Factor V: Build Release Run ??? CI/CD pipeline

# === Factor V: Build, Release, Run ===

# GitHub Actions CI/CD Pipeline
cat > .github/workflows/ci-cd.yml << 'EOF'
name: Blazor CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  DOTNET_VERSION: '8.0.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: }

jobs:
  # BUILD stage
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: }
      
      - name: Restore
        run: dotnet restore
      
      - name: Build
        run: dotnet build --no-restore -c Release
      
      - name: Test
        run: dotnet test --no-build -c Release --verbosity normal
      
      - name: Publish
        run: dotnet publish MyApp.Web -c Release -o publish
      
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: blazor-app
          path: publish/

  # RELEASE stage (Docker)
  release:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      
      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          registry: }
          username: }
          password: }
      
      - name: Build & Push Docker
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            }/}:}
            }/}:latest

  # RUN stage (Deploy)
  deploy:
    needs: release
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/blazor-app \
            web=}/}:}
          kubectl rollout status deployment/blazor-app
EOF

echo "CI/CD pipeline configured"

Logging, Monitoring ????????? Disposability

Factor IX ????????? XI: Disposability ????????? Logs

// === Factor IX: Disposability & Factor XI: Logs ===

// File: MyApp.Web/HealthChecks/AppHealthCheck.cs
namespace MyApp.Web.HealthChecks;

using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

// Factor IX: Fast startup, graceful shutdown
public class AppHealthCheck : IHealthCheck
{
    private readonly ILogger<AppHealthCheck> _logger;

    public AppHealthCheck(ILogger<AppHealthCheck> logger)
    {
        _logger = logger;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        // Check database connectivity
        var isHealthy = true; // Replace with actual check
        
        if (isHealthy)
        {
            _logger.LogDebug("Health check passed");
            return Task.FromResult(HealthCheckResult.Healthy("OK"));
        }
        
        _logger.LogWarning("Health check failed");
        return Task.FromResult(HealthCheckResult.Unhealthy("Database unavailable"));
    }
}

// Factor XI: Structured Logging
// File: MyApp.Core/Services/OrderService.cs
namespace MyApp.Core.Services;

using Microsoft.Extensions.Logging;

public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public async Task<OrderResult> ProcessOrder(int orderId)
    {
        using var scope = _logger.BeginScope(
            new Dictionary<string, object>
            {
                ["OrderId"] = orderId,
                ["CorrelationId"] = Guid.NewGuid().ToString()
            });

        _logger.LogInformation("Processing order {OrderId}", orderId);
        
        try
        {
            // Process order
            await Task.Delay(100); // Simulate work
            
            _logger.LogInformation(
                "Order {OrderId} processed successfully, Amount: {Amount}",
                orderId, 1500.00m);
            
            return new OrderResult { Success = true, OrderId = orderId };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Failed to process order {OrderId}: {Error}",
                orderId, ex.Message);
            
            return new OrderResult { Success = false, Error = ex.Message };
        }
    }
}

public class OrderResult
{
    public bool Success { get; set; }
    public int OrderId { get; set; }
    public string? Error { get; set; }
}

// Factor IX: Graceful Shutdown in Program.cs
// app.Lifetime.ApplicationStopping.Register(() =>
// {
//     logger.LogInformation("Application shutting down gracefully");
//     // Drain connections, finish in-flight requests
// });

FAQ ??????????????????????????????????????????

Q: Blazor Server ????????? Blazor WebAssembly ??????????????????????????????????

A: Blazor Server render ????????? server ????????? UI updates ???????????? SignalR (WebSocket) ??????????????? initial load ????????????, ????????????????????? server resources ??????????????????, ?????????????????? thin clients ????????? ????????????????????? ???????????? connection ????????????, latency ????????????????????? network, server load ???????????????????????? users ???????????? Blazor WebAssembly (WASM) ??????????????? browser ??????????????? offline ?????????, ????????????????????? server connection, CDN deploy ????????? ????????????????????? initial download ???????????? (2-5MB), ?????????????????????????????? server resources ????????? Blazor United (.NET 8) ?????????????????????????????? ??????????????? render mode ????????? component ??????????????????????????????????????????????????? project ????????????

Q: 12-Factor ?????????????????????????????????????

A: ?????????????????????????????????????????? modern cloud applications 12-Factor ??????????????? Portable deploy ?????????????????? cloud (Azure, AWS, GCP, on-premise), Scalable scale horizontally ?????????????????????, Maintainable ????????? concerns ??????????????????, CI/CD friendly build, release, run ??????????????????, Container ready ????????????????????? Docker/Kubernetes ??????????????? ?????????????????????????????????????????????????????? 12 ???????????????????????? ???????????????????????? Config (Factor III), Dependencies (Factor II), Logs (Factor XI) ???????????? ????????????????????????????????????????????????????????????

Q: Blazor ?????? state management ????????????????????? ?????????????????? Factor VI (Stateless)?

A: Factor VI ???????????????????????? processes ???????????? stateless ????????????????????? state ?????? memory ?????????????????? Blazor Server ????????? Scoped services ?????????????????? per-circuit state (ok ?????????????????? session), Redis/Database ?????????????????? shared state (?????????????????????????????? 12-Factor), Cascading Parameters ?????????????????? component tree, Fluxor (Flux/Redux pattern) ?????????????????? complex state ?????????????????? Blazor WASM ????????? browser localStorage/sessionStorage, State containers (in-memory ok ?????????????????????????????? browser), API calls ?????????????????? persistent state ???????????? state ????????????????????? persist ?????????????????????????????? backing service (database, Redis) ?????????????????? process memory

Q: Deploy Blazor ?????? Kubernetes ????????????????????????????

A: ??????????????? Docker image ????????? Dockerfile ?????????????????????????????????????????? (multi-stage build), ??????????????? Kubernetes manifests Deployment, Service, Ingress, ConfigMap, Secret ????????????????????? environment variables ???????????? ConfigMap/Secret (Factor III), ????????? Health checks (liveness + readiness) ?????????????????? Factor IX, ???????????? HPA (Horizontal Pod Autoscaler) ?????????????????? Factor VIII (Concurrency), ????????? Helm chart ?????????????????? deployment ???????????? environments ?????????????????? Blazor Server ???????????????????????? sticky sessions (SignalR ???????????? connect ???????????? server ????????????) ????????? Kubernetes Ingress annotation nginx.ingress.kubernetes.io/affinity: cookie

📖 บทความที่เกี่ยวข้อง

Vue Nuxt Server 12 Factor Appอ่านบทความ → LVM Thin Provisioning 12 Factor Appอ่านบทความ → AWS App Runner สำหรับมือใหม่ Step by Stepอ่านบทความ → HAProxy Advanced 12 Factor Appอ่านบทความ → OAuth 2.1 12 Factor Appอ่านบทความ →

📚 ดูบทความทั้งหมด →