# Olet luultavasti tekemässä EF-siirtolaisuutta väärin...

Suoritetaan `MigrateAsync()` Startupissa? Annat sovellustietokannan omistajalle oikeudet ja toivot, että mikään ei mene pieleen. On olemassa parempi tapa - EF migration -nippujen avulla voit pyörittää muuttoliikkeitä kontrolloituna CI-askeleena ja pitää tuotantosovelluksesi turvallisena. Mutta joskus "väärä" tapa on itse asiassa hyvä. Tutkitaan, milloin kannattaa käyttää jokaista lähestymistapaa.

<datetime class="hidden">2025-11-23T18:39</datetime>

<!--category--  Entity Framework, Migrations, GitHub, CI -->
**Viralliset dokumentit:** [Maahanmuuttoa koskeva yleiskatsaus](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) | [Muuttoliikkeiden soveltaminen](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) | [Punokset](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#bundles)

[TOC]

# "Väärin" tapa (jota käytän)

Tämä blogi käyttää `MigrateAsync()` Startup - lähestymistapa, jota aion kieltää käyttämästä. Tämän vuoksi se sopii minulle, ja miksi se ei todennäköisesti ole sinulle.

Omassa `Program.cs` Minulla on seuraavat tiedot:

```csharp
    using (var scope = app.Services.CreateScope())
    {
        var blogContext = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
        await blogContext.Database.MigrateAsync();
    }
```

[`MigrateAsync()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.migrateasync) soveltaa vireillä olevia siirtoja ja luo tarvittaessa tietokannan. Yksinkertainen - mutta ongelmallinen:

1. **Käynnistysriippuvuus** Sovelluksesi ei käynnisty.
2. **Turvallisuusrikkomus** - Sovelluksesi tarvitsee `db_owner` Annoit juuri aika-applikaatiollesi avaimet pudottaa pöytiä.

Miksi pääsen pälkähästä: julkinen data, yksittäinen Docker-verkko, henkilökohtainen projekti. **Et varmaankaan pysty.**

## Kun nopeat muuttoliikkeet ovat kunnossa

- **Paikallinen dev** - Nopea iterointi voittaa seremonian
- **Henkilökohtaiset projektit** - Alhainen räjähdyssäde, ei arkaluontoisia tietoja
- **Docker-compose-devo-ympäristöt** Kätevyys voittaa
- **Prototyypitys** - Schema muuttuu joka tapauksessa jatkuvasti

## Kun he eivät ole

- **Useita applikaatioita** - Rotuolosuhteet galore
- **Herkät tiedot** - PII, taloudellinen, säännelty = asianmukainen erottaminen
- **Tuotanto todellisilla käyttäjillä** - Epäonnistunut muutto = käyttökatkos

# Oikea tapa: EF Bundles

EF-nippu on omatoiminen suoritin, joka sisältää kootut muuttoliikkeesi. `dotnet ef database update` pakataan itsenäiseksi `.exe`.

**Miksi nippuja voittaa:**

- **Ei ajoajan riippuvuuksia** - Kohde ei tarvitse SDK:ta tai EF CLI:tä
- **Asianmukainen erottaminen** - Sovellus ei koskaan tarvitse `db_owner`; vain CI-juoksijalla on, vain käyttöönoton aikana
- **CI-näkyvyys** - Epäonnistumiset näkyvät putkiloissa, eivät sovelluksen käynnistykseen hautautuneina
- **Kääntymisturvallisuus** Maahanmuutto epäonnistuu, ennen kuin huonot koodit alkavat.
- **Idempotentti** - Seuraa mitä sovelletaan, kulkee vain mitä tarvitaan

> **Huomaa:** Tuotantoluokan varmuuteen, käyttöön [Hallittu henkilöllisyys](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) Kytkentänarujen sijaan, mutta nippu on yhä merkittävä askel eteenpäin.

## GitHub-toiminnot esimerkki

```yaml
      - name: Install EF Core tools
        run: dotnet tool install --global dotnet-ef

      - name: Add EF tools to PATH
        run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH

      - name: Generate EF migration bundle
        run: |
          dotnet ef migrations bundle \
            --project ${{ env.WEB_PROJECT }} \
            --output efbundle.exe \
            --configuration ${{ env.BUILD_CONFIGURATION }} \
            --runtime ${{ env.RUNTIME_IDENTIFIER }} \
            --context AdminDbContext \
        env:
          AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}

      - name: Run EF migration bundle
        run: |
          ./efbundle.exe
        env:
          AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}
```

Nippu lukee yhteyden naruja ympäristömuuttujista ja soveltaa vireillä olevia muuttoliikkeitä. Jo käytössä? Se vain poistuu onnistuneesti.

# Paikalliset bundlet

Ei tiedonantajaa, haluatko testata ennen työntämistä? Rakenna nippuja paikallisesti.

**Käyttötapaukset:** Testaa ennen CI:n, DBA:n luovutusta (itse asiassa exe, ei SDK:ta tarvita), lavastusten käyttöönottoa, vianetsintää `--verbose`.

## Bundlen luominen

```bash
# Install EF CLI (once)
dotnet tool install --global dotnet-ef

# Basic bundle
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe

# Self-contained (includes runtime - portable to machines without .NET)
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe \
    --self-contained

# Cross-platform (e.g., build on Windows, deploy to Linux)
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle \
    --runtime linux-x64
```

## Bundlen pyörittäminen

```bash
# Using default connection string from appsettings.json
./efbundle.exe

# Override with a specific connection string
./efbundle.exe --connection "Host=localhost;Database=mostlylucid;Username=postgres;Password=secret"

# Using an environment variable (matches your config key)
$env:ConnectionStrings__DefaultConnection="Host=localhost;..." # PowerShell
export ConnectionStrings__DefaultConnection="Host=localhost;..." # Bash
./efbundle.exe
```

## Hyödyllisiä bundle-vaihtoehtoja

```bash
# See what migrations would be applied without running them
./efbundle.exe --dry-run

# Verbose output for debugging
./efbundle.exe --verbose

# Apply migrations up to a specific migration (useful for testing)
./efbundle.exe --target-migration "20231115_AddUserTable"

# Combine options
./efbundle.exe --verbose --dry-run
```

## Paikallinen testaus Työnkulku

```bash
# 1. Create migration
dotnet ef migrations add AddNewFeature \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid

# 2. Build bundle
dotnet ef migrations bundle \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid \
    --output efbundle.exe

# 3. Dry run first
./efbundle.exe --dry-run --verbose

# 4. Run for real
./efbundle.exe --verbose

# 5. Broken? Remove and retry
dotnet ef migrations remove \
    --project Mostlylucid.DbContext \
    --startup-project Mostlylucid
```

Nappaa syntaksivirheitä, rajoitusrikkomuksia, FK-asioita - kaikki *ennen* CI tai tuotanto.

## Rakenna suorituskykyä

**Bundle-sukupolvi on hidas** - 30 sekuntia isoissa projekteissa.

- Luo käsin, kun testaat paikallisesti
- Luo CI:ssä vain käyttöönoton aikana, ei jokaisen PR:n aikana
- Välimuistin nippuja, jos muuttoliikkeet eivät ole muuttuneet

Jos todella haluat autogeneraatiota, lisää MSBuild-kohde:

```xml
<Target Name="BuildMigrationBundle">
  <Exec Command="dotnet ef migrations bundle --output $(OutputPath)efbundle.exe --force" />
</Target>
```

Sitten: `dotnet build -t:BuildMigrationBundle`

# Hybridilähestymistapa

Paras molemmista maailmoista: mukavuus paikallisesti, tuotannon turvallisuus.

```csharp
if (builder.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var context = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
    await context.Database.MigrateAsync();
}
// Production: CI pipeline runs the bundle
```

# Vaihtoehtoja bundleille

## SQL-skriptit

Luo tavallinen SQL suoritettavan sijaan. Loistava DBA-arviointiin ja olemassa oleviin muutoshallintaprosesseihin.

```bash
# All migrations
dotnet ef migrations script --output migrations.sql

# Idempotent (safe to run multiple times) - USE THIS
dotnet ef migrations script --idempotent --output migrations.sql

# Range of migrations
dotnet ef migrations script FromMigration ToMigration --output migrations.sql
```

**Plussat:** Täysi näkyvyys, mikä tahansa SQL-asiakas voi ajaa sitä, versiohallinta ystävällinen, DBA-hyväksynnän työnkulku.

**Miinukset:** Ei automaattijäljitystä (käytä `--idempotent`), manuaalinen suoritus, mahdollinen drift, jos skriptejä muutetaan.

Katso [viralliset dokumentit SQL-skripteistä](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#sql-scripts).

### SQL Scripts in CI

```yaml
- name: Generate and apply migrations
  run: |
    dotnet ef migrations script --idempotent --output migrations.sql
    # SQL Server
    sqlcmd -S ${{ secrets.DB_SERVER }} -d ${{ secrets.DB_NAME }} -i migrations.sql
    # Or PostgreSQL
    PGPASSWORD=${{ secrets.DB_PASSWORD }} psql -h ${{ secrets.DB_HOST }} -f migrations.sql
```

## DACTAC (vain SQL-palvelin)

[DACPAC-keskukset](https://learn.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications) a *valtiolähtöinen* ei *Muuttoliikkeeseen perustuva*Määrität halutun skeeman, ja SqlPackage vertaa sitä kohdetietokantaan.

```bash
SqlPackage.exe /Action:Publish /SourceFile:MyDatabase.dacpac /TargetConnectionString:"..."
```

**Plussat:** Schema koodina, automaattisena diff-sukupolvena, hoitaa kaiken (taulukot, näkymät, SP:t, indeksit), yritystyökalut.

**Miinukset:** Vain SQL Server, schema kahdessa paikassa (EF-mallit + SQL-projekti), diff-moottori tekee kyseenalaisia valintoja, sarake uudelleennimet näyttävät pudotus+lisältä.

Katso [SqlPackage-dokumentit](https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage).

## Vertailutaulukko

Autoraiteet Sovellettava DBA Friendly Cross-platform DB
|----------|----------|---------------|---------------------|--------------|-------------------|
| `MigrateAsync()` Kyllä, kyllä, kyllä, kyllä, kyllä
EF Bundles CI/CD-putkistot
SQL:n käsikirjoitukset DBA:n hallitsemissa ympäristöissä `--idempotent` Kyllä, kyllä, kyllä.
DACPAC, SQL Server enterprise Yes (valtiollinen) Kyllä

# Vinkkejä

## Suunnittelija-tiedosto Gotcha

Muuttoliikkeet toimivat paikallisesti, mutta eivät tiedonantajana? **Tarkista, että kirjoitit molemmat tiedostot:**

- `20231115_AddUserTable.cs` - Maahanmuuttokoodi
- `20231115_AddUserTable.Designer.cs` - Mallikuva

Designer-tiedoston puuttuminen = äänetön epäonnistuminen.

## Useita DbContextejä

```bash
dotnet ef migrations bundle --context BlogDbContext --output blog-migrations.exe
dotnet ef migrations bundle --context IdentityDbContext --output identity-migrations.exe
```

## Liitännät etusijalle

1. `--connection` argumentti
2. Ympäristömuuttuja
3. `appsettings.json`

Käytä ympäristömuuttujia CI:ssä.

## IDesignTimeDbContextFactory

Jos DbContext on erillisessä projektissa tai siinä on monimutkainen käynnistys, toteuta [`IDesignTimeDbContextFactory<T>`](https://learn.microsoft.com/en-us/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-a-design-time-factory):

```csharp
public class AdminDbContextFactory : IDesignTimeDbContextFactory<AdminDbContext>
{
    public AdminDbContext CreateDbContext(string[] args)
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true)
            .AddEnvironmentVariables()
            .AddUserSecrets<AdminDbContextFactory>()
            .Build();

        var connectionString = config["AdminSite:ConnectionString"]
            ?? throw new InvalidOperationException("Missing connection string");

        var optionsBuilder = new DbContextOptionsBuilder<AdminDbContext>();
        optionsBuilder.UseSqlServer(connectionString, sql => sql.CommandTimeout(120));

        return new AdminDbContext(optionsBuilder.Options);
    }
}
```

Käytä, kun: DbContext erillisessä projektissa, monimutkainen käynnistys, tarvitsee käyttäjäsalaisuuksia suunnitteluaikaan.

# Entä...?

Olen saanut yleisiä kysymyksiä ja vastatoimia.

## "Miksei vain juosta `dotnet ef database update` CI:ssä?"

Katettu yllä, mutta lyhyt versio: niput ovat kannettavia esineitä. Käyttöönottovaihe ei tarvitse EF CLI:tä, lähdekoodia tai suunnitteluajan tarkkuutta. Sama nippu kulkee testissä, lavastuksessa ja prod - nolla-ajossa.

## "Eikö tämä ole liioittelua pienelle sovellukselle?"

Jos olet yksin, data on julkista, ja räjähdyssäde on matala. `MigrateAsync()` Mutta heti kun lisäät toisen kehittäjän, arkaluonteisen datan tai useita ympäristöjä, nippu maksaa itsensä.

## "Entä rollbackit?"

EF ei tee automaattikäännöksiä.

- Luo a `Down()` Muuttoliike ja sen pyörittäminen (mutta sinun täytyy olla kirjoittanut se)
- Palauta varmuuskopiosta
- Kirjoita manuaalinen siirtymä, jolla muutokset perutaan

Kriittisten järjestelmien osalta testataan ensin migraatiota tietokantakloonia vastaan.

## "Saanko pyörittää muuttoliikkeitä Kuberneettien kontissa?"

Kyllä. Bundle + init -pakkaus on kiinteä kuvio:

```yaml
initContainers:
  - name: migrate
    image: myapp:latest
    command: ["./efbundle.exe"]
    env:
      - name: ConnectionStrings__Default
        valueFrom:
          secretKeyRef:
            name: db-secrets
            key: connection-string
```

Applikaattori odottaa, että se valmistuu.

## "Entä FluentMigrator / DbUp / muut työkalut?"

Ne toimivat hyvin. EF-nippu on EF-natiivi ratkaisu, mutta [FluentMigraattori](https://fluentmigrator.github.io/) sekä [DbUp](https://dbup.readthedocs.io/) Avainero: ne ovat muuttoliikkeelle ominaisia työkaluja, kun taas EF-nippu on peräisin nykyisestä EF-mallistasi.

## "DBA haluaa tarkistaa SQL:n ennen kuin se toimii."

Käyttö `--idempotent` käsikirjoitukset:

```bash
dotnet ef migrations script --idempotent --output migrations.sql
```

DBA arvioi ja hyväksyy. Sitten joko:

- Suorita skripti käsin, tai
- Kun se on hyväksytty, pyöritä nippua (joka toimii samoin)

## "Miten käsittelen muuttoliikkeitä, joissa ei ole vapaa-aikaa?"

Se on käyttöönottostrategiakysymys, ei maahanmuuttokysymys.

1. Tee siirtymisestä takaperin yhteensopivaa (lisää sarakkeita mitättömäksi, älä nimeä uudelleen)
2. Ota käyttöön uusi koodi, joka käsittelee sekä vanhaa että uutta skeemaa
3. Suorita muuttoliikkeet
4. Käytä koodia, joka käyttää vain uutta skeemaa
5. Siivotkaa (pudottakaa vanhat palstat myöhempään muuttoon)

Bundlet eivät ratkaise tätä, ne vain tekevät vaiheesta 3 ennustettavamman.