Technology

C# Entity Framework Zero Downtime Deployment

c entity framework zero downtime deployment
C# Entity Framework Zero Downtime Deployment | SiamCafe Blog
2025-12-07· อ. บอม — SiamCafe.net· 1,634 คำ

C# Entity Framework Zero Downtime Deployment คืออะไร

Entity Framework (EF) Core เป็น ORM (Object-Relational Mapper) หลักของ .NET ที่ช่วย developers ทำงานกับ database ผ่าน C# objects แทนการเขียน SQL โดยตรง Zero Downtime Deployment คือการ deploy application version ใหม่โดยไม่มี downtime — ผู้ใช้ไม่รู้สึกว่าระบบหยุดทำงาน ความท้าทายหลักคือ database migrations ที่ต้อง backward compatible เพราะระหว่าง deploy มีทั้ง version เก่าและใหม่ทำงานพร้อมกัน บทความนี้อธิบายวิธี deploy EF Core applications แบบ zero downtime พร้อมตัวอย่าง code

Zero Downtime Deployment Strategies

# zdt_strategies.py — Zero downtime deployment strategies
import json

class ZDTStrategies:
    STRATEGIES = {
        "rolling": {
            "name": "Rolling Deployment",
            "description": "Deploy ทีละ instance — old + new versions run พร้อมกัน",
            "requirement": "Database schema ต้อง compatible กับทั้ง 2 versions",
            "tools": "Kubernetes rolling update, Azure App Service slots",
        },
        "blue_green": {
            "name": "Blue-Green Deployment",
            "description": "Deploy version ใหม่ (Green) แยก → switch traffic → retire Blue",
            "requirement": "Database shared ระหว่าง Blue + Green → migration ต้อง backward compatible",
            "tools": "Kubernetes services, AWS ALB, Azure Traffic Manager",
        },
        "canary": {
            "name": "Canary Deployment",
            "description": "ส่ง traffic 5-10% ไป version ใหม่ → monitor → ค่อยเพิ่ม",
            "requirement": "เหมือน Rolling — schema ต้อง compatible",
            "tools": "Istio, Flagger, AWS App Mesh",
        },
    }

    CHALLENGES = {
        "schema_change": "Schema changes (add/remove columns) ต้อง backward compatible",
        "data_migration": "Data migration ต้องไม่ lock tables นาน",
        "rollback": "ต้อง rollback ได้ถ้า version ใหม่มีปัญหา",
        "connection_pool": "Connection pool ต้องไม่ขาด ระหว่าง deploy",
        "ef_migration": "EF Migrations ต้อง idempotent + backward compatible",
    }

    def show_strategies(self):
        print("=== ZDT Strategies ===\n")
        for key, strat in self.STRATEGIES.items():
            print(f"[{strat['name']}]")
            print(f"  {strat['description']}")
            print(f"  Requirement: {strat['requirement']}")
            print()

    def show_challenges(self):
        print("=== Challenges ===")
        for key, challenge in self.CHALLENGES.items():
            print(f"  [{key}] {challenge}")

zdt = ZDTStrategies()
zdt.show_strategies()
zdt.show_challenges()

EF Core Migration Best Practices

# ef_migrations.py — EF Core migration patterns for ZDT
import json

class EFMigrationPatterns:
    SAFE_OPERATIONS = """
// === SAFE Operations (backward compatible) ===

// 1. ADD column (nullable or with default)
migrationBuilder.AddColumn(
    name: "MiddleName",
    table: "Users",
    type: "nvarchar(100)",
    nullable: true);  // nullable = safe

// 2. ADD table
migrationBuilder.CreateTable(
    name: "UserPreferences",
    columns: table => new {
        Id = table.Column(nullable: false)
            .Annotation("SqlServer:Identity", "1, 1"),
        UserId = table.Column(nullable: false),
        Theme = table.Column(maxLength: 50, nullable: true),
    });

// 3. ADD index (CONCURRENTLY on PostgreSQL)
migrationBuilder.CreateIndex(
    name: "IX_Users_Email",
    table: "Users",
    column: "Email");

// 4. RENAME via expand-contract pattern
// Step 1 (deploy v1.1): Add new column
migrationBuilder.AddColumn(
    name: "FullName", table: "Users", nullable: true);
// Step 2 (deploy v1.2): Copy data + use new column
// Step 3 (deploy v1.3): Drop old column
"""

    UNSAFE_OPERATIONS = """
// === UNSAFE Operations (cause downtime) ===

// ❌ DROP column — old version still reads it
migrationBuilder.DropColumn(name: "OldField", table: "Users");

// ❌ RENAME column — old version can't find it
migrationBuilder.RenameColumn(
    name: "Name", table: "Users", newName: "FullName");

// ❌ Change column type — may lose data
migrationBuilder.AlterColumn(
    name: "Age", table: "Users", type: "int");

// ❌ Add NOT NULL column without default
migrationBuilder.AddColumn(
    name: "RequiredField", table: "Users", nullable: false);
// Old version inserts without this field → error!
"""

    def show_safe(self):
        print("=== Safe Operations ===")
        print(self.SAFE_OPERATIONS[:600])

    def show_unsafe(self):
        print("\n=== Unsafe Operations ===")
        print(self.UNSAFE_OPERATIONS[:500])

patterns = EFMigrationPatterns()
patterns.show_safe()
patterns.show_unsafe()

Expand-Contract Pattern

# expand_contract.py — Expand-Contract migration pattern
import json

class ExpandContractPattern:
    PATTERN = """
// === Expand-Contract Pattern for Column Rename ===
// Goal: Rename "Name" → "FullName" without downtime

// === Phase 1: EXPAND (Deploy v2.0) ===
// Add new column, keep old column
public partial class AddFullNameColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Add new column (nullable)
        migrationBuilder.AddColumn(
            name: "FullName",
            table: "Users",
            type: "nvarchar(200)",
            nullable: true);
        
        // Copy existing data
        migrationBuilder.Sql(
            "UPDATE Users SET FullName = Name WHERE FullName IS NULL");
        
        // Add trigger to sync (optional)
        migrationBuilder.Sql(@"
            CREATE TRIGGER trg_SyncFullName ON Users
            AFTER INSERT, UPDATE AS
            BEGIN
                UPDATE u SET u.FullName = i.Name
                FROM Users u INNER JOIN inserted i ON u.Id = i.Id
                WHERE u.FullName IS NULL OR u.FullName != i.Name
            END");
    }
}

// App v2.0: Read from FullName, write to BOTH Name + FullName
// Old app v1.x: Still reads/writes Name — works fine

// === Phase 2: MIGRATE (Deploy v2.1) ===
// App reads/writes only FullName
// Verify all data migrated

// === Phase 3: CONTRACT (Deploy v2.2) ===
public partial class DropNameColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Drop trigger
        migrationBuilder.Sql("DROP TRIGGER IF EXISTS trg_SyncFullName");
        
        // Drop old column (safe — no app reads it anymore)
        migrationBuilder.DropColumn(name: "Name", table: "Users");
    }
}
"""

    TIMELINE = [
        "Deploy v2.0: Add FullName column + sync trigger (old app works)",
        "Deploy v2.1: App uses FullName only (old column still exists)",
        "Verify: All data migrated, no reads on old column",
        "Deploy v2.2: Drop old Name column + trigger (cleanup)",
    ]

    def show_pattern(self):
        print("=== Expand-Contract Pattern ===")
        print(self.PATTERN[:600])

    def show_timeline(self):
        print(f"\n=== Deployment Timeline ===")
        for step in self.TIMELINE:
            print(f"  → {step}")

ec = ExpandContractPattern()
ec.show_pattern()
ec.show_timeline()

Python Migration Validator

# validator.py — Validate EF migrations for ZDT safety
import json

class MigrationValidator:
    CODE = """
# ef_migration_validator.py — Check if migrations are ZDT-safe
import re
import json
from pathlib import Path

class MigrationSafetyChecker:
    UNSAFE_PATTERNS = {
        'DropColumn': {
            'pattern': r'migrationBuilder\\.DropColumn',
            'severity': 'critical',
            'message': 'DROP COLUMN breaks old app versions reading this column',
            'fix': 'Use expand-contract: add new column → migrate → drop old',
        },
        'RenameColumn': {
            'pattern': r'migrationBuilder\\.RenameColumn',
            'severity': 'critical',
            'message': 'RENAME COLUMN breaks old app versions',
            'fix': 'Use expand-contract: add new → copy data → drop old',
        },
        'DropTable': {
            'pattern': r'migrationBuilder\\.DropTable',
            'severity': 'high',
            'message': 'DROP TABLE may break old app versions',
            'fix': 'Only drop after all app versions stop using this table',
        },
        'AlterColumn_NotNull': {
            'pattern': r'nullable:\\s*false',
            'severity': 'high',
            'message': 'Adding NOT NULL constraint may fail for existing rows',
            'fix': 'Add with default value or make nullable first',
        },
        'RenameTable': {
            'pattern': r'migrationBuilder\\.RenameTable',
            'severity': 'critical',
            'message': 'RENAME TABLE breaks old app versions',
            'fix': 'Create new table → migrate data → drop old',
        },
    }
    
    def check_file(self, filepath):
        '''Check a single migration file for unsafe patterns'''
        content = Path(filepath).read_text()
        issues = []
        
        for name, rule in self.UNSAFE_PATTERNS.items():
            matches = re.findall(rule['pattern'], content)
            if matches:
                issues.append({
                    'rule': name,
                    'severity': rule['severity'],
                    'message': rule['message'],
                    'fix': rule['fix'],
                    'occurrences': len(matches),
                })
        
        return {
            'file': str(filepath),
            'safe': len(issues) == 0,
            'issues': issues,
        }
    
    def check_directory(self, migrations_dir):
        '''Check all migration files in directory'''
        results = []
        for f in sorted(Path(migrations_dir).glob('*.cs')):
            if 'Designer' not in f.name:
                results.append(self.check_file(f))
        
        total = len(results)
        safe = sum(1 for r in results if r['safe'])
        unsafe = total - safe
        
        return {
            'total_migrations': total,
            'safe': safe,
            'unsafe': unsafe,
            'details': [r for r in results if not r['safe']],
        }

# checker = MigrationSafetyChecker()
# result = checker.check_directory("./Migrations")
# print(json.dumps(result, indent=2))
"""

    def show_code(self):
        print("=== Migration Validator ===")
        print(self.CODE[:600])

validator = MigrationValidator()
validator.show_code()

CI/CD Pipeline

# cicd.py — CI/CD pipeline for ZDT with EF Core
import json

class ZDTPipeline:
    GITHUB_ACTIONS = """
# .github/workflows/deploy-zdt.yml
name: Zero Downtime Deploy
on:
  push:
    branches: [main]

jobs:
  validate-migrations:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0'
      
      - name: Check Migration Safety
        run: |
          python scripts/check_migrations.py ./src/Migrations
          if [ $? -ne 0 ]; then
            echo "UNSAFE migrations detected!"
            exit 1
          fi
      
      - name: Run Tests
        run: dotnet test --configuration Release

  deploy:
    needs: validate-migrations
    runs-on: ubuntu-latest
    steps:
      - name: Apply Migrations
        run: |
          dotnet ef database update \\
            --connection "}" \\
            --project src/MyApp
      
      - name: Rolling Deploy
        run: |
          kubectl set image deployment/myapp \\
            myapp=myapp:} \\
            --record
          kubectl rollout status deployment/myapp \\
            --timeout=300s
      
      - name: Smoke Test
        run: |
          curl -f https://myapp.example.com/health || exit 1
      
      - name: Rollback on Failure
        if: failure()
        run: |
          kubectl rollout undo deployment/myapp
"""

    DEPLOY_ORDER = [
        "1. Validate: Check migration safety (no unsafe patterns)",
        "2. Test: Run unit + integration tests with new schema",
        "3. Migrate: Apply DB migrations (backward compatible)",
        "4. Deploy: Rolling update — old + new run together",
        "5. Verify: Health checks + smoke tests",
        "6. Monitor: Watch error rates, latency for 15 min",
        "7. Cleanup: Next deploy — remove old columns/tables if needed",
    ]

    def show_pipeline(self):
        print("=== CI/CD Pipeline ===")
        print(self.GITHUB_ACTIONS[:500])

    def show_order(self):
        print(f"\n=== Deployment Order ===")
        for step in self.DEPLOY_ORDER:
            print(f"  {step}")

pipeline = ZDTPipeline()
pipeline.show_pipeline()
pipeline.show_order()

FAQ - คำถามที่พบบ่อย

Q: EF Migration ต้อง run ก่อน deploy app ใหม่หรือเปล่า?

A: ใช่ — ต้อง migrate database ก่อน deploy app version ใหม่ เพราะ: app ใหม่อาจต้องการ columns/tables ใหม่ที่ migration สร้าง ลำดับ: 1) Apply migration → 2) Deploy new app → 3) Old app ยัง run ได้ (backward compatible) สำคัญ: migration ต้อง backward compatible — old app version ต้อง work กับ schema ใหม่ได้

Q: ถ้า migration ผิดพลาด rollback ยังไง?

A: EF Core: dotnet ef database update [PreviousMigrationName] — rollback ไป migration ก่อนหน้า แต่: ถ้า migration ทำ data transformation — rollback อาจสูญเสียข้อมูล Best practice: backup database ก่อน migrate เสมอ ป้องกัน: test migration บน staging ก่อน production + ใช้ expand-contract pattern

Q: Expand-Contract ต้องใช้กี่ deploys?

A: ขั้นต่ำ 3 deploys: Deploy 1 (Expand): เพิ่ม column ใหม่ + sync data Deploy 2 (Migrate): app ใช้ column ใหม่เท่านั้น Deploy 3 (Contract): ลบ column เก่า ข้อเสีย: ช้ากว่า deploy เดียว แต่ไม่มี downtime เลย เหมาะกับ: production systems ที่ downtime ยอมรับไม่ได้

Q: EF Core กับ Dapper อันไหนดีสำหรับ ZDT?

A: EF Core: มี migration system built-in, schema ผูกกับ model — เปลี่ยน model = ต้อง migrate Dapper: ไม่มี migration (ใช้ FluentMigrator/DbUp แทน), SQL เขียนเอง — flexible กว่า สำหรับ ZDT: ทั้งสองทำได้ — สำคัญคือ migration strategy ไม่ใช่ ORM EF Core ง่ายกว่า: migration tooling ดี, model-first approach Dapper ยืดหยุ่นกว่า: ควบคุม SQL ได้เต็มที่

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

C# Minimal API Zero Downtime Deploymentอ่านบทความ → C# Entity Framework CQRS Event Sourcingอ่านบทความ → Databricks Unity Catalog Zero Downtime Deploymentอ่านบทความ → C# Entity Framework Clean Architectureอ่านบทความ → C# Entity Framework IoT Gatewayอ่านบทความ →

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