Back to "Multiplattform AOT med SQLite: Hur får man det att fungera!"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

.NET AOT DevOps SQLite

Multiplattform AOT med SQLite: Hur får man det att fungera!

Thursday, 12 December 2024

Om du har försökt att skicka .NET-program som enda, inhemska körbara med Native AOT, har du förmodligen köra ansikte-först in i SQLite väggen. Du får allt konfigurerat, bygget slutförs framgångsrikt, och sedan-boom-runtime kraschar med kryptiskt DllNotFoundException fel om e_sqlite3. Låt mig spara dig timmar av frustration jag gick igenom och visa dig exakt hur man får SQLite arbetar med Native AOT över Windows, Linux (inklusive ARM64 Raspberry Pi), och macOS.

Vad är AOT? (Och varför skulle du bry dig?)

Låt oss börja med de absoluta grunderna. Om du någonsin har byggt en .NET-applikation och undrar varför du behöver installera ".NET Runtime" på servrar eller varför din konsol app tar en andra för att starta första gången du kör den, AOT är svaret på dessa problem.

Hur .NET fungerar normalt (JIT kompilering)

När du skriver C#-kod och bygger din applikation, producerar kompilatorn inte maskinkod som din CPU kan köra direkt. Istället, Det producerar något som kallas Mellanspråk (IL)-tänk på det som halvvägs mellan din C#-kod och faktiska maskininstruktioner.

När du kör din .NET app, här är vad som händer:

  1. Din dator laddar .NET Runtime (ett separat program)
  2. Körtiden läser din IL-kod
  3. Just-in-time (JIT) kompilator konverterar IL till inhemsk maskinkod när din app körs
  4. Din CPU kör äntligen den där maskinkoden

Detta är som att ha en översättare som läser ditt recept (IL) och verbalt översätter det till en kock (CPU) linje för linje medan de lagar mat. Det fungerar, men det finns overhead:

  • .NET Runtime är 50-150MB extra filer du behöver för att distribuera
  • JIT-kompilatorn tar sig tid att översätta koden (varför din app är långsam första gången)
  • JIT-kompilatorn själv sitter i minnet medan appen körs

Ange AOT (för-tid-sammanställning)

Native AOT vänder denna modell på huvudet. Istället för att översätta din kod på runtime, det översätter allt vid byggtiden. Du slutar med en enda körbar fil som innehåller faktiska maskinkod din CPU kan köra direkt – ingen körtid, ingen översättare, ingen väntan.

Tänk på det som att få en professionellt översatt receptbok i stället för att anställa en live översättare. Arbetet görs en gång i förväg, och resultatet är redo att användas omedelbart.

De spel-Changing fördelar

Här är vad Native AOT ger dig:

1. Små körbara program: 10-30MB istället för 150MB+

Din app och allt den behöver kompileras i en liten binär. Inga separata runtime-filer.

2. Snabbstart: 80% snabbare kallstart

Mina tester: normala .NET tog ~800ms för att starta, AOT tog ~150ms. Det finns ingen JIT uppvärmningstid – maskinkoden är redo att köra omedelbart.

3. Noll beroenden: Nej . NET körtid krävs

Du kan kopiera din körbar till vilken maskin som helst med rätt OS (Windows/Linux/Mac) och den körs bara. Ingen "installera .NET 9 Runtime"-förutsättning.

4. Lägre minnesanvändning: Ca 50% mindre minne

Ingen JIT kompilator sitter i minnet. I min gateway program, normal .NET använde 85MB tomgång, AOT använde 42MB.

5. Bättre för begränsade miljöer: Fungerar där JIT inte kan

Vissa miljöer (vissa Docker behållare, iOS, inbyggda system) tillåter inte körning kodgenerering. AOT fungerar överallt.

De kompromisser (Det finns alltid en fångst)

AOT är inte magi. Du handlar med runtime flexibilitet för förhandsoptimering:

1. Ingen dynamisk kodgenerering

Allt som genererar kod vid runtime kommer inte att fungera:

  • System.Reflection.Emit (skapar dynamiska typer)
  • Dynamisk monteringsbelastning (laddning av DLL-filer vid körning)
  • Några fina reflektionstrick

De flesta normala .NET-kod är bra, men vissa ramar som är starkt beroende av reflektion behöver särskild konfiguration.

2. Plattformsspecifika byggen

JIT sammanställning producerar IL som körs på någon plattform. AOT producerar inhemsk maskinkod för En särskild plattform. Du måste bygga separat för:

  • Fönster x64
  • Linux x64
  • Linux ARM64 (Raspberry Pi)
  • macOS Intel
  • macOS Apple Silicon

Vi tar hand om det här med GitHub-åtgärder senare.

3. Längre byggtider

I stället för att kompilera till IL på några sekunder kompilerar AOT hela vägen till maskinkod. Räkna med 2-5 minuter istället för 10 sekunder. Det är en engångskostnad för permanenta fördelar.

4. Vissa funktioner behöver extra konfiguration

JSON serialisering, Entity Framework, och allt som använder tung reflektion kan behöva dig att uttryckligen tala om för kompilatorn vilka typer att behålla. Vi täcker detta.

När bör du använda AOT?

Native AOT är perfekt för:

  • CLI-verktyg: Command-line verktyg där omedelbar start frågor
  • Mikrotjänster: Mindre Docker bilder, snabbare skalning i Kubernetes
  • Serverlös/Lambda: Kall starttid direkt påverkar din räkning
  • Kantanordningar: Raspberry Pi, IoT-enheter med begränsade resurser
  • Gateway-tillämpningar: Omvända proxies, API-gateways med hög genomströmning
  • Skrivbordsverktyg: Skicka en enda .exe utan "installera .NET första" steg

Hoppa över AOT för:

  • Traditionella webbappar där starttid inte spelar någon roll
  • Appar som använder tunga Entity Framework med många migreringar
  • Insticksprogram som laddar DLL-filer dynamiskt
  • Allt som genererar kod vid körning

För CLI-verktyg, mikrotjänster, behållare och kantenheter ändras förbättringarna. Men det finns en fångst när du lägger till databaser – specifikt SQLite.

SQLite - problemet

Nu när du förstår vad AOT är, låt oss tala om den enskilt största smärtpunkten: SQLite. Detta är väggen de flesta människor träffas när man försöker använda AOT med databas-backed program.

Vad som händer (Den frustrerande upplevelsen)

Här är den typiska resan:

  1. Du lägger till Microsoft.Data.Sqlite till ditt projekt
  2. Du ställer in PublishAot=true i din .csproj
  3. Bygget fullbordas utan fel – allt ser bra ut!
  4. Du kör kör den körbara och omedelbart få:
DllNotFoundException: Unable to load DLL 'e_sqlite3' or one of its dependencies

Din app kraschar innan den kan göra något.

Förstå Native Libraries (En snabb avstickare)

För att förstå problemet, måste du veta om Inhemska bibliotek.

SQLite är inte skrivet i .NET-det är skrivet i C. Det är sammanställt till plattformsspecifik infödd kod:

  • e_sqlite3.dll om fönster
  • libe_sqlite3.so på Linux
  • libe_sqlite3.dylib om macOS

När du använder Microsoft.Data.Sqlite I vanliga .NET, det är bara ett omslag runt detta inhemska SQLite bibliotek. dynamisk belastning lämplig infödd fil för din plattform.

Detta fungerar bra med normala .NET eftersom:

  1. Körtiden kan ladda bibliotek dynamiskt
  2. NuGet-paket kan inkludera infödda bibliotek för alla plattformar
  3. Den rätta blir vald vid körning

Varför AOT bryter detta

Inhemska AOT har två egenskaper som strider mot SQLites strategi:

1. Aggressiv trimning: AOT tar bort någon kod som den tror att du inte använder. Om det inte kan statiskt bevisa att du behöver något, blir det bort. Dynamisk bibliotek laddning förvirrar trimmer-det kan inte se kopplingen mellan din kod och den inhemska SQLite DLL.

2. Inget dynamiskt laststöd: AOT producerar en fristående binär. Den förväntar sig att alla inhemska beroenden uttryckligen kopplas vid kompileringstid, inte laddad dynamiskt vid körning.

Resultatet: Microsoft.Data.Sqlite förväntar sig att hitta en infödd SQLite-bibliotek på runtime, men AOT har antingen klippt bort det eller inte vet hur man buntar det ordentligt.

Mardrömmen om det tillfälliga beroendet

Ännu värre, om du har andra NuGet-paket som använder SQLite (som vissa ORM-bibliotek eller mina mostlylucid.ephemeral.complete paket), kan du sluta med flera inkompatibla SQLite-leverantörer i ditt beroendeträd.

Varje leverantör försöker arbeta på olika sätt:

  • Man kan förvänta sig OS-tillhandahållen SQLite (endast Windows)
  • En annan kan bunta sin egen SQLite
  • En annan kan använda en annan infödd biblioteksversion

Den AOT kompilatorn blir förvirrad om vilken man ska använda, och ofta resultatet är att det inkluderar ingen av dem – eller värre, inkluderar motstridiga filer som inte kan arbeta tillsammans.

Lösningen: Använd bunten

Efter timmar av frustration, hittade jag lösningen: SQLitePCLRaw.bundle_e_sqlite3. Detta är en speciell NuGet paket som utformats särskilt för att arbeta med AOT.

Lägg till dessa två paket till ditt projekt:

<ItemGroup>
  <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
  <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
</ItemGroup>

Vad gör bundeln?

och bundle_e_sqlite3 Paketet skiljer sig från vanliga SQLite leverantörer:

1. Den innehåller förkompilerade infödda SQLite bibliotek för varje större plattform:

  • Fönster (x64, x86, ARM64)
  • Linux (x64, ARM64, musl-baserade distros som alpina)
  • macOS (x64 Intel, ARM64 Apple Silicon)

2. Dessa infödda bibliotek är förpackade på ett sätt som AOT kompilatorn förstår

Paketet markerar sina ursprungliga beroenden explicit så att AOT kompilatorn vet att inkludera dem i den slutliga binären. Ingen dynamisk laddning, ingen körning söker efter filer.

3. Den är utformad för plattformsoberoende byggen

Ett paket fungerar för alla plattformar. Du behöver inte plattformsspecifika paket eller villkorliga referenser.

Varför detta fungerar

Minns du de två problemen vi identifierade?

Problem 1: AOT:s trimning tar bort biblioteken det kan inte se att användas

  • Lösning: Paketet förklarar uttryckligen sina egna bibliotek som byggtillgångar som måste inkluderas

Problem 2: AOT stöder inte dynamisk laddning

  • Lösning: Paketet länkar statiskt SQLite-biblioteken vid kompileringstid

Resultatet: när du bygger för Windows x64, innehåller bunten e_sqlite3.dll. När du bygger för Linux ARM64, innehåller det ARM64 libe_sqlite3.soAllt fungerar bara.

Kritiska: Initiera bundeln ( Skippa inte detta!)

Här är den del som reser upp 90% av människor som försöker använda SQLite med AOT, inklusive mig på mitt första försök.

Med normalt .NET, SQLite bunten automatiskt initierar sig själv första gången du använder SQLite. Magi händer bakom kulisserna-du behöver inte göra något.

Med Native AOT, denna automatiska initiering fungerar inte. AOT kompilatorn kan inte se den automatiska startkoden (det ser ut som oanvänd kod och blir trimmad), så du måste initieras manuellt bunten i början av din ansökan.

Här är den magiska linjen du behöver:

using SQLitePCL;

public class Program
{
    public static void Main(string[] args)
    {
        // THIS IS CRITICAL: Initialize SQLite FIRST, before ANYTHING else
        SQLitePCL.Batteries.Init();

        // Now you can do normal application setup
        var builder = WebApplication.CreateBuilder(args);

        // This is now safe - SQLite is initialized
        builder.Services.AddDbContext<MyDbContext>(options =>
            options.UseSqlite("Data Source=app.db"));

        var app = builder.Build();
        app.Run();
    }
}

Vad gör Batteries.Init() Gör?

Denna metod säger SQLite bunt till:

  1. Hitta rätt inbyggt SQLite-bibliotek för din nuvarande plattform
  2. Ladda det i minnet
  3. Koppla upp alla anslutningar mellan Microsoft.Data.Sqlite och det inhemska biblioteket

Det kallas "Batterier" eftersom det är "Batterier ingår" bunt – allt du behöver är förpackat tillsammans.

Var man kan sätta den

Lägg Batteries.Init() som den allra första raden i din Main Metod före:

  • Skapa applikationsbyggaren
  • Anpassa beroendeinjektion
  • Öppna alla databasanslutningar
  • Läsa konfigurationsfiler som kan använda SQLite

Tänk på det som att koppla in en enhet innan du försöker slå på den. Om du försöker använda SQLite innan du ringer Init(), du kommer fortfarande att få DllNotFoundException även om DLL är korrekt buntad i din ansökan.

Vad händer om du glömmer?

Om du glömmer att ringa Batteries.Init(), kommer din app:

  1. Bygg framgångsrikt (kompilatorn kommer inte att varna dig)
  2. Börja köra
  3. Krascha ögonblicket det försöker använda SQLite med:
DllNotFoundException: Unable to load DLL 'e_sqlite3' or one of its dependencies

Detta är förvirrande eftersom DLL är Jag förlorade två timmar till detta misstag. Var inte som jag.

Fullständig projektinställning

Här är en full .csproj Konfigurerad för multi-plattform Native AOT med SQLite:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <!-- Native AOT -->
    <PublishAot>true</PublishAot>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>
    <InvariantGlobalization>true</InvariantGlobalization>

    <!-- Single File -->
    <PublishSingleFile>true</PublishSingleFile>
    <StripSymbols>true</StripSymbols>

    <!-- Optimization -->
    <OptimizationPreference>Speed</OptimizationPreference>

    <!-- Multi-platform targets -->
    <RuntimeIdentifiers>
      win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64
    </RuntimeIdentifiers>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
  </ItemGroup>

</Project>

Nyckelinställningar förklarade (på klarspråk)

Låt mig dela upp vad var och en av dessa inställningar gör:

PublishAot=true

Detta är master switch som möjliggör Native AOT sammanställning. Utan detta får du normala .NET beteende (JIT sammanställning). Med detta får du i förväg kompilerad infödd kod.

PublishTrimmed=true och TrimMode=full

Dessa säger till kompilatorn att ta bort någon kod du inte använder. Tänk på det som att städa ur garaget innan du flyttar – varför packa saker du inte behöver?

  • PublishTrimmed=true möjliggör trimning
  • TrimMode=full betyder "vara aggressiv, ta bort allt du kan"

Varning: Detta kan bryta koden som använder tung reflektion (som några JSON serializers eller ORMs) eftersom trimmern inte alltid kan se vad du använder via reflektion. Vi täcker hur man hanterar detta senare.

InvariantGlobalization=true

Detta tar bort alla kulturspecifika data från din app – datumformat, valutasymboler, textsorteringsregler för olika språk. Sparar 5-10MB.

Ställ bara in det här till true om din app:

  • Använder endast engelska
  • Behöver inte kulturspecifik datum-/tidformatering
  • Använder endast ordinalsträngsjämförelser (byte-by-byte)

Om du bygger ett CLI-verktyg eller API-gateway som inte bryr sig om lokalisering, är detta gratis besparingar. Om du bygger något som behöver formatera datum för franska användare eller sortera turkisk text korrekt, hoppa över denna inställning.

PublishSingleFile=true

Bundlar allt till en körbar fil. Istället för att ha:

myapp.exe
myapp.dll
System.Text.Json.dll
... 50 more files

Du får bara:

myapp.exe

Mycket lättare att använda.

StripSymbols=true

Felsökningssymboler hjälper felsökare att visa dig variabla namn och radnummer vid felsökning. De är användbara under utvecklingen men lägger till flera megabyte till din sista binär.

Denna inställning tar bort dem. Din app körs exakt samma, bara mindre.

OptimizationPreference=Speed

Detta talar om för kompilatorn vad man ska prioritera när man fattar beslut:

  • Speed: Gör det snabbt (lite större binärer, men bättre prestanda)
  • Size: Gör det litet (lite långsammare, men minimal binär storlek)

För de flesta tillämpningar, Speed är rätt val. Storleksskillnaden är oftast bara 2-3MB, men prestandaskillnaden kan vara märkbar.

RuntimeIdentifiers

Detta förklarar vilka plattformar du vill stödja. Det bygger inte alla – det säger bara verktyg "dessa är giltiga mål."

Tillgängliga identifierare:

  • win-x64: Windows 64-bit (Intel/AMD)
  • win-arm64: Windows ARM64 (Surface Pro X, etc.)
  • linux-x64: Linux 64-bit (Ubuntu, Debian, RHEL, etc.)
  • linux-arm64: Linux ARM64 (Raspberry Pi 4/5, AWS Graviton)
  • osx-x64: macOS Intel (äldre Mac)
  • osx-arm64: macOS Apple Silicon (M1/M2/M3 Macs)

Du bygger en plattform åt gången med hjälp av dotnet publish -r linux-x64, till exempel.

Bygga för flera plattformar

Minns du hur jag sa att AOT kräver plattformsspecifika byggen? Du måste kompilera separat för Windows, Linux x64, Linux ARM64, macOS Intel och macOS Apple Silicon. Att göra detta manuellt skulle vara tråkigt – men vi kan automatisera det.

Vad är GitHub-åtgärder?

Om du inte känner till GitHub Actions är en gratis CI/CD-tjänst (Continuous Integration/Continuous Deployment) inbyggd i GitHub. Den låter dig köra automatiserade uppgifter när du trycker på koden eller skapar en releasetag.

Tänk på det som att ha en byggserver som:

  1. Tittar på ditt GitHub arkiv
  2. När du trycker på en tagg som v1.0.0
  3. Det snurrar automatiskt upp Windows, Linux och macOS virtuella maskiner
  4. Bygger din app för alla plattformar parallellt
  5. Skapar en GitHub Release med alla bifogade binärer

Allt detta körs på GitHubs servrar – du behöver inte underhålla någon infrastruktur. För projekt med öppen källkod och små personliga projekt är det helt gratis.

Det kompletta byggarbetsflödet

Här är ett arbetsflöde för GitHub Actions som automatiskt bygger för alla större plattformar:

name: Build Native AOT Binaries

on:
  push:
    tags:
      - 'v*'

jobs:
  build-binaries:
    name: Build ${{ matrix.runtime }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            runtime: linux-x64
            artifact-name: myapp-linux-x64

          - os: ubuntu-latest
            runtime: linux-arm64
            artifact-name: myapp-linux-arm64

          - os: windows-latest
            runtime: win-x64
            artifact-name: myapp-win-x64

          - os: macos-latest
            runtime: osx-x64
            artifact-name: myapp-osx-x64

          - os: macos-latest
            runtime: osx-arm64
            artifact-name: myapp-osx-arm64

    steps:
    - uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '9.0.x'

    - name: Install ARM64 tools (Linux ARM64 only)
      if: matrix.runtime == 'linux-arm64'
      run: |
        sudo apt-get update
        sudo apt-get install -y clang zlib1g-dev gcc-aarch64-linux-gnu

    - name: Publish
      shell: bash
      run: |
        # Set objcopy for ARM64 cross-compilation
        if [ "${{ matrix.runtime }}" = "linux-arm64" ]; then
          OBJCOPY_PARAM="-p:ObjCopyName=aarch64-linux-gnu-objcopy"
        else
          OBJCOPY_PARAM=""
        fi

        dotnet publish \
          -c Release \
          -r ${{ matrix.runtime }} \
          --self-contained \
          --output ./publish/${{ matrix.runtime }} \
          -p:PublishAot=true \
          -p:PublishTrimmed=true \
          -p:StripSymbols=true \
          $OBJCOPY_PARAM

    - name: Create distribution package
      shell: bash
      run: |
        mkdir -p ./dist
        cd ./publish/${{ matrix.runtime }}

        # Copy the main executable
        cp myapp${{ matrix.file-ext }} ../../dist/

        # CRITICAL: Copy native libraries (SQLite and any other native dependencies)
        # PublishSingleFile bundles .NET code, but native DLLs remain separate
        cp *.dll ../../dist/ 2>/dev/null || true
        cp *.so ../../dist/ 2>/dev/null || true
        cp *.dylib ../../dist/ 2>/dev/null || true

        # Copy config files if needed
        cp ../../appsettings.json ../../dist/ || true

        cd ../../dist

        # Create archive with all files
        if [ "${{ runner.os }}" = "Windows" ]; then
          7z a -tzip ../${{ matrix.artifact-name }}.zip *
        else
          tar czf ../${{ matrix.artifact-name }}.tar.gz *
        fi

    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: ${{ matrix.artifact-name }}
        path: |
          ${{ matrix.artifact-name }}.zip
          ${{ matrix.artifact-name }}.tar.gz
        retention-days: 7
        if-no-files-found: ignore

Hur detta arbetsflöde fungerar (steg för steg)

Om YAML ser skrämmande ut, så här gör den på engelska:

1. Utlösare (on: push: tags)

Arbetsflödet körs när du trycker på en Git-tagge som börjar med v (som v1.0.0, v2.3.1. . Detta är det vanliga sättet att markera versionerna.

2. Matrisstrategi

Detta är den smarta delen. I stället för att skriva fem separata arbetsflöden, definierar vi en matris Byggnader:

  • Varje byggnad får en annan os (föregångsmaskin) och runtime (målplattform)
  • GitHub-åtgärder kör alla fem byggen parallellt
  • Var och en producerar en artefakt (kompilerade binära)

Så när du trycker v1.0.0, GitHub samtidigt:

  • Spins upp en Ubuntu-maskin för att bygga Linux x64 och ARM64
  • Spins upp en Windows-maskin för att bygga Windows x64
  • Spins upp en macOS-maskin för att bygga macOS x64 och ARM64

3. Steg i varje bygge

Varje plattform bygger gör samma steg:

  1. Utcheckningskod: Hämtar källkoden från arkivet
  2. Ställ in .NET: Installerar .NET 9 SDK
  3. Installera verktyg för ARM64 (endast Linux ARM64): Installerar verktyg för korskompilering
  4. Publicera: Kör dotnet publish med AOT-flaggor för den specifika plattformen
  5. Ladda upp artefakt: Sparar kompilerad binär så att nästa jobb kan komma åt det

4. Resultatet

När alla bygg färdig, du har fem artefakter (binärer) redo att distribuera. Du kan ladda ner dem från åtgärderna kör, eller använda ett andra jobb för att skapa en GitHub Release automatiskt (inte visas i denna text, men lätt att lägga till).

Kritiska: Inhemska bibliotek är inte förenade

Här är något som förvirrat mig i timmar: PublishSingleFile=true Endast paket .NET-kod. Inhemska bibliotek som SQLite's e_sqlite3.dll Håll dig ifrån varandra.

Därför är steget "Skapa distributionspaket" så viktigt:

# Copy native libraries - these are NOT included in the main executable
cp *.dll ../../dist/ 2>/dev/null || true    # Windows
cp *.so ../../dist/ 2>/dev/null || true     # Linux
cp *.dylib ../../dist/ 2>/dev/null || true  # macOS

och 2>/dev/null || true Del betyder "om det inte finns några filer som matchar detta mönster, misslyckas inte – fortsätt bara." Det låter samma skript fungera på alla plattformar.

Vad du distribuerar:

  • Fönster: myapp.exe + e_sqlite3.dll (tillsammans i en ZIP)
  • Linux: Vad är det? myapp + libe_sqlite3.so (tillsammans i en tjär.gz)
    • Vad är det med dig? myapp + libe_sqlite3.dylib (tillsammans i en tjär.gz)

Användare extraherar arkivet och kör kör det körbara. Det ursprungliga SQLite-biblioteket ligger bredvid det körbara, och bunten hittar det automatiskt vid körtid.

Varför manuell triggning är användbart

Lägg märke till workflow_dispatch utlösning högst upp:

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to publish'
        required: true

Detta låter dig utlösa byggen manuellt från GitHubs webbgränssnitt utan att trycka på en tagg. Användbart för att testa arbetsflödet eller skapa beta-byggen.

ARM64 Korskompilering

Linux ARM64 bygger kräver särskild uppmärksamhet. Du behöver korskompileringsverktyg och måste ange rätt objcopy Verktyg:

# Install tools
sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

# Build with objcopy specified
dotnet publish \
  -r linux-arm64 \
  -p:PublishAot=true \
  -p:ObjCopyName=aarch64-linux-gnu-objcopy

Utan ObjCopyName parameter, länken misslyckas med kryptiska fel om oigenkännliga filformat.

Verkliga resultat

Här är vad jag uppnådde med min produktion YARP-baserade bot detektering gateway med middleware och SQLite loggning. Detta är ett riktigt projekt du kan nedladdning från GitHub.

Binära storlekar (från faktiska GitHub-utgåvor)

Det här är Verkliga filstorlekar från min GitHub releaser, inte teoretiska uppskattningar:

till Plattform och körbar på SQLite Native DLL till Total Archive Size |----------|-----------|-------------------|-------------------| på Windows x64 och 9.2MB på +1,7MB 10,9 Mbyte (ZIP) (ZIP) (ZIP) (ZIP) (ZIP)) på Linux x64 och 10.8MB på +1.6MB 12,4 Mbyte (tar.gz) till .. Linux ARM64 .9.9MB .. +1.5MB .. 11,4 Mbyte (tar.gz) till till macOS Intel på 11,2MB på +1,8MB 13,0 Mbyte (tar.gz) till MACOS ARM64 och 9.8 Mbyte +1,7 Mbyte . 11,5 Mbyte (tar.gz) till

Jämför detta med fristående .NET 9 utbyggnader vid 130-150 MB per plattform. Vi pratar om en 10-12x minskning av storlek.

Fördelningen:

  • Huvudkörbar: Din kompilerade kod + .NET körtid (AOT-kompilerad)
  • Native DLL: SQLite databasmotorn (skriven i C)

Båda filerna måste distribueras tillsammans, men de är fortfarande dramatiskt mindre än traditionella .NET-distributioner.

Uppstartsprestanda

Kall start (första begäran serverad) på en blygsam Linux VPS:

  • Självförsörjande .NET: ~800ms
  • Inhemsk AOT: ~150ms
  • Förbättring: 81% snabbare

Detta är av stor betydelse för följande:

  • Serverless/Lambda-funktioner (du betalar per millisekund)
  • Behållare som ofta skalas upp/ner
  • CLI-verktyg där varje anrop börjar fräscht

Minnesanvändning

Tomgångsminne (gateway körs, ingen trafik):

  • Självförsörjande .NET: 85 MB
  • Inhemsk AOT: 42MB
  • Förbättring: 51 % minskning

Under belastning (1000 förfrågningar/sekund):

  • Självförsörjande .NET: : : : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  • Inhemsk AOT: ~180MB
  • Förbättring: 44 % minskning

Lägre minne innebär:

  • Fler behållare per värd
  • Billigare moln hosting räkningar
  • Genomförbarhet på resursbegränsade enheter (Raspberry Pi, IoT)

Vanliga fall

1. Glömma Batteries.Init()

Detta är det # 1 misstaget. Även med bunten korrekt refererade, glömma att ringa SQLitePCL.Batteries.Init() i början av din Main metoden kommer att orsaka runtime krascher.

Rättningen:

public static void Main(string[] args)
{
    SQLitePCL.Batteries.Init(); // FIRST LINE
    // ... rest of your code
}

2. Inte distribuera Native Libraries

Det här är misstag nummer 2, och det fick mig två gånger. PublishSingleFile=true buntar INTE in inhemska SQLite- DLLer i din körbara. Du måste distribuera dem tillsammans med din körbara.

Vad händer om du glömmer:

  • Din app bygger bra
  • Det går bra på din dev maskin (där SQLite kanske redan är installerad)
  • Det kraschar på produktionen med DllNotFoundException

Rättningen:

När du packar din release, alltid inkludera:

myapp.exe               # Your main executable
e_sqlite3.dll           # SQLite native library (Windows)
# or libe_sqlite3.so    # SQLite native library (Linux)
# or libe_sqlite3.dylib # SQLite native library (macOS)

I dina GitHub-åtgärder eller distributionsskript:

# Copy ALL native libraries from the publish directory
cp *.dll ./dist/ 2>/dev/null || true
cp *.so ./dist/ 2>/dev/null || true
cp *.dylib ./dist/ 2>/dev/null || true

Användare bör extrahera arkivet och köra körbar. Det ursprungliga biblioteket måste vara i samma katalog.

3. Användning winsqlite3 Leverantör

Du kan se rekommendationer att använda SQLitePCLRaw.provider.winsqlite3 på Windows för att använda OS-tillhandahållen SQLite. Inte. Det fungerar bara på Windows, kräver manuell initiering, och bryter plattformsoberoende byggen. bundle_e_sqlite3.

4. Trimvarningar med EF Core

Om du använder Entity Framework Core med SQLite får du trimvarningar (IL2026, IL3050). Dessa är vanligtvis säkra att undertrycka för EF Core s SQLite leverantör, men testa noggrant:

<PropertyGroup>
  <NoWarn>$(NoWarn);IL2026;IL3050</NoWarn>
</PropertyGroup>

Ännu bättre, överväga att använda Dapper eller rå ADO.NET med SQLite för AOT-program-de är mer AOT-vänlig.

5. Saknar visuella studioverktyg (Windows)

På Windows, Native AOT kräver Visual Studio s MSVC länkare. Om du bygger utanför en utvecklarkommando Prompt, får du fel om vswhere.exe. GitHub Åtgärder hanterar detta automatiskt, men för lokala byggen, använd:

  • Utvecklarkommando Prompt för VS 2022
  • Utvecklare PowerShell för VS 2022

Eller initiera miljön i byggskriptet:

& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
dotnet publish -c Release -r win-x64

När du ska använda Native AOT

Native AOT är perfekt för:

  • CLI-verktyg: Snabbstart frågor
  • Mikrotjänster och containrar: Mindre bilder, snabbare skalning
  • Kantanordningar: Raspberry Pi, IoT-enheter med begränsade resurser
  • Serverlös/Lambda: Kall starttid är avgörande
  • Gateway/proxy-tillämpningar: Högt genomflöde, lågt övertryck

Undvik AOT för:

  • Heavy Entity Framework Core-användning (många migreringar, komplexa frågor)
  • Reflektionstunga tillämpningar
  • Dynamiska instickssystem
  • Tillämpningar som behöver runtime-kodgenerering

Checklista för snabbstart

Om du har läst hela detta inlägg och bara vill ha en checklista att följa, här går du:

1. Lägg till paketen:

<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />

2. Anpassa din .csproj för AOT:

<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishSingleFile>true</PublishSingleFile>
<StripSymbols>true</StripSymbols>
<OptimizationPreference>Speed</OptimizationPreference>

3. Initiera SQLite i början av din Main Metod:

SQLitePCL.Batteries.Init();

4. Bygg för din målplattform:

dotnet publish -c Release -r linux-x64 --self-contained

5. Testa din binär – det ska bara köras utan beroenden!

Slutsatser

Att få SQLite att arbeta med Native AOT är inte uppenbart, men när man väl känner till den magiska besvärjelsenSQLitePCLRaw.bundle_e_sqlite3 + Batteries.Init()Vinsten är betydande: små binärer, omedelbar start och möjligheten att placera ut till vilken plattform som helst utan driftstidsberoenden.

För alla som bygger CLI-verktyg, gateways eller edge-applikationer med .NET är Native AOT med SQLite nu ett realistiskt alternativ. arbetsflödet GitHub Actions som tillhandahålls här automatiserar hela multiplattformsbyggprocessen – tagga bara en release och du är klar.

Om du är ny på AOT, börja små: konvertera en enkel CLI verktyg eller verktyg först. Bli bekväm med byggprocessen, lära dig vilka varningar att förvänta sig, och förstå begränsningarna. När du har fått grunderna nere, kan du hantera mer komplexa program.

.NET ekosystem är allt mer AOT-vänlig. De flesta moderna bibliotek antingen fungerar ur lådan eller har tydlig dokumentation på AOT stöd. Framtiden är infödd, och det är snabbare än du tror.

Verklig erfarenhet av utplacering

Jag använder min AOT-kompilerade port till:

  • Digital oceandropp (Linux x64) - körs som en systemd tjänst
  • Hallon Pi 5 (Linux ARM64) - Kantutbyggnad för testning
  • Windows- serverName (Windows x64) - körs som en Windows-tjänst
  • Behållare för dockor - både x64 och ARM64 varianter

Utplaceringsprocessen är densamma överallt:

  1. Ladda ner arkivet från GitHub releaser
  2. Extrahera den
  3. Kör det körbara programmet

Inget "installera .NET Runtime" steg. Inget beroende helvete. Inga versionskonflikter. Bara extrahera och köra.

För Docker, min Dockerfile är pinsamt enkelt:

FROM debian:bookworm-slim

# Copy just the two files we need
COPY minigw /app/minigw
COPY libe_sqlite3.so /app/libe_sqlite3.so

WORKDIR /app
RUN chmod +x minigw

EXPOSE 5000
ENTRYPOINT ["./minigw"]

Den resulterande bilden är Om ja, ange i tillämpliga fall vilka åtgärder som ska vidtas för att säkerställa att de åtgärder som föreskrivs i denna förordning är förenliga med den inre marknaden. (huvudsakligen Debians basavbildning). En traditionell .NET-behållare skulle vara 200-250 MB.

Fullständigt arbetsexempel

Allt jag har visat här kommer från ett riktigt, produktionsklart projekt.

Projektet är en minimal YARP omvänd proxy med bot upptäckt middleware som loggar signaturer till SQLite. Det visar:

  • Inhemsk AOT med SQLite
  • Multiplattformsbyggen via GitHub-åtgärder
  • Inhemsk biblioteksbundling
  • CLI-argumentets tolkning
  • Strukturerad loggning
  • Förtjusande avstängning

Klona den, studera den, använd den som mall för dina egna AOT-projekt.

Resurser

Gå och bygg något litet och snabbt.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.