# Multiplataforma AOT con SQLite: ¡Cómo hacer que funcione!

<!--category -- .NET, AOT, SQLite, DevOps -->
<datetime class="hidden">2025-12-14T15:00</datetime>

Si usted ha estado tratando de enviar aplicaciones .NET como ejecutables únicos y nativos usando AOT nativo, usted probablemente ha ejecutado cara-primero en la pared SQLite. Usted consigue todo configurado, la construcción se completa con éxito, y luego —boom— tiempo de ejecución se bloquea con críptico `DllNotFoundException` errores sobre `e_sqlite3`. Déjame ahorrarte las horas de frustración que pasé y mostrarte exactamente cómo hacer que SQLite trabaje con Native AOT a través de Windows, Linux (incluyendo ARM64 Raspberry Pi), y macOS.

## ¿Qué es AOT? (¿Y por qué debería importarte?)

Empecemos con los conceptos básicos absolutos. Si alguna vez has construido una aplicación .NET y te has preguntado por qué necesitas instalar el ".NET Runtime" en los servidores o por qué tu aplicación de consola tarda un segundo en empezar la primera vez que lo ejecutas, AOT es la respuesta a esos problemas.

### Cómo .NET funciona normalmente (compilación JIT)

Cuando escribes código C# y construyes tu aplicación, el compilador no produce código de máquina que tu CPU pueda ejecutar directamente. En su lugar, produce algo llamado **Lengua intermedia (IL)**—pensar en ello como a mitad de camino entre el código C# y las instrucciones reales de la máquina.

Cuando ejecutes tu aplicación .NET, esto es lo que sucede:

1. Su ordenador carga el .NET Runtime (un programa separado)
2. El tiempo de ejecución lee su código IL
3. **Justo a tiempo (JIT)** compilador convierte IL a código nativo de la máquina a medida que su aplicación se ejecuta
4. Su CPU finalmente ejecuta ese código de máquina

Esto es como tener un traductor que lee su receta (IL) y la traduce verbalmente a un chef (CPU) línea por línea mientras están cocinando. Funciona, pero hay arriba:

- El .NET Runtime es de 50-150MB de archivos adicionales que necesita implementar
- El compilador JIT toma tiempo para traducir código (por qué tu aplicación es lenta la primera vez)
- El compilador JIT en sí mismo se encuentra en la memoria mientras que la aplicación se ejecuta

### Introduzca AOT (compilación anticipada)

Native AOT voltea este modelo en su cabeza. En lugar de traducir su código en tiempo de ejecución, se traduce todo **en el tiempo de construcción**. Terminas con un único archivo ejecutable que contiene código de máquina real que tu CPU puede ejecutar directamente: sin tiempo de ejecución, sin traductor, sin espera.

Piense en ello como conseguir un libro de recetas traducido profesionalmente en lugar de contratar a un traductor en vivo. El trabajo se hace una vez, por adelantado, y el resultado está listo para usar inmediatamente.

### Los beneficios de cambiar de juego

Esto es lo que el AOT nativo te da:

**1. Pequeños ejecutables**: 10-30MB en lugar de 150MB+

Su aplicación y todo lo que necesita se compila en un pequeño binario. No hay archivos de tiempo de ejecución separados.

**2. Inicio instantáneo**: 80% más rápido comienza el frío

Mis pruebas: normal .NET tomó ~800ms para comenzar, AOT tomó ~150ms. No hay tiempo de calentamiento JIT — el código de la máquina está listo para ejecutar inmediatamente.

**3. Cero dependencias**: No se requiere tiempo de funcionamiento .NET

Puede copiar su ejecutable a cualquier máquina con el sistema operativo correcto (Windows/Linux/Mac) y simplemente se ejecuta. No hay requisito previo para "instalar .NET 9 Runtime".

**4. Menor uso de memoria**: Alrededor del 50% menos de memoria

No hay compilador JIT sentado en la memoria. En mi aplicación de puerta de enlace, normal .NET utilizado 85MB inactivo, AOT utilizado 42MB.

**5. Mejor para entornos restringidos**: Funciona donde JIT no puede

Algunos entornos (algunos contenedores Docker, iOS, sistemas integrados) no permiten la generación de código de tiempo de ejecución. AOT funciona en todas partes.

### Los trueques (Siempre hay una trampa)

AOT no es magia, estás intercambiando flexibilidad de tiempo de ejecución para la optimización inicial:

**1. No hay generación de código dinámico**

Cualquier cosa que genere código en tiempo de ejecución no funcionará:

- `System.Reflection.Emit` (creando tipos dinámicamente)
- Carga dinámica de montaje (carga de DLLs en tiempo de ejecución)
- Algunos trucos de reflexión de lujo

El código .NET más normal está bien, pero algunos marcos que dependen en gran medida de la reflexión necesitan una configuración especial.

**2. Estructuras específicas de la plataforma**

JIT compilation produce IL que se ejecuta en cualquier plataforma. AOT produce código de máquina nativo para **una plataforma específica**. Usted necesita construir por separado para:

- Ventanas x64
- Linux x64
- Linux ARM64 (Raspberry Pi)
- macOS Intel
- macOS Apple Silicon

Cubriremos automatizando esto con GitHub Actions más tarde.

**3. Tiempos de construcción más largos**

En lugar de compilar a IL en segundos, AOT compila todo el camino al código de la máquina. Espere 2-5 minutos en lugar de 10 segundos. Es un costo de una sola vez para los beneficios permanentes.

**4. Algunas características necesitan configuración adicional**

La serialización de JSON, Entity Framework y cualquier cosa que utilice reflexión pesada puede necesitarle que le diga explícitamente al compilador qué tipos guardar.

### ¿ Cuándo debe usar AOT?

El AOT nativo es perfecto para:

- **Herramientas CLI**: Utilidades de línea de comandos donde el inicio instantáneo importa
- **Microservicios**: Imágenes Docker más pequeñas, escalado más rápido en Kubernetes
- **Sin servidor/Lambda**: El tiempo de inicio frío afecta directamente su factura
- **Dispositivos de borde**: Raspberry Pi, dispositivos IoT con recursos limitados
- **Aplicaciones de pasarela**: Proxies inversos, pasarelas API con alto rendimiento
- **Herramientas de escritorio**: Enviar un solo .exe sin "instalar .NET primero" paso

Saltar AOT para:

- Aplicaciones web tradicionales donde el tiempo de inicio no importa
- Aplicaciones que utilizan el pesado Entity Framework con muchas migraciones
- Sistemas de complementos que cargan DLLs dinámicamente
- Cualquier cosa que genere código en tiempo de ejecución

Para herramientas CLI, microservicios, contenedores y dispositivos de borde, las mejoras están cambiando. Pero hay una captura cuando se agregan bases de datos, específicamente SQLite.

## El problema SQLite

Ahora que usted entiende lo que es AOT, vamos a hablar sobre el punto de dolor más grande: SQLite. Esta es la pared que la mayoría de la gente golpea cuando intenta utilizar AOT con aplicaciones respaldadas por bases de datos.

### Lo que sucede (la experiencia frustrante)

Aquí está el típico viaje:

1. Añadir `Microsoft.Data.Sqlite` a su proyecto
2. Configure `PublishAot=true` en tu `.csproj`
3. La construcción se completa sin errores, ¡todo se ve bien!
4. Ejecutas el ejecutable e inmediatamente obtienes:

```
DllNotFoundException: Unable to load DLL 'e_sqlite3' or one of its dependencies
```

Tu aplicación se bloquea antes de que pueda hacer cualquier cosa. ¿Qué pasa?

### Comprensión de las bibliotecas nativas (un desvío rápido)

Para entender el problema, usted necesita saber acerca de **bibliotecas nativas**.

SQLite no está escrito en .NET; está escrito en C. Está compilado en código nativo específico de la plataforma:

- `e_sqlite3.dll` en Windows
- `libe_sqlite3.so` en Linux
- `libe_sqlite3.dylib` en macOS

Cuando use `Microsoft.Data.Sqlite` en normal .NET, es sólo un envoltorio alrededor de esta biblioteca SQLite nativa. En tiempo de ejecución, trata de **cargar dinámicamente** el archivo nativo apropiado para su plataforma.

Esto funciona bien con .NET normal porque:

1. El tiempo de ejecución puede cargar bibliotecas dinámicamente
2. Los paquetes NuGet pueden incluir bibliotecas nativas para todas las plataformas
3. El correcto se selecciona en tiempo de ejecución

### ¿Por qué AOT rompe esto?

El AOT nativo tiene dos características que chocan con el enfoque de SQLite:

**1. Recortes agresivos**: AOT elimina cualquier código que crea que no está utilizando. Si no puede demostrar estáticamente que necesita algo, se elimina. Carga de biblioteca dinámica confunde el trimmer - no puede ver la conexión entre su código y el DLL SQLite nativo.

**2. No hay soporte de carga dinámico**: AOT produce un binario autónomo. Espera que todas las dependencias nativas se vinculen explícitamente en el tiempo de compilación, no se carguen dinámicamente en el tiempo de ejecución.

El resultado: `Microsoft.Data.Sqlite` espera encontrar una biblioteca SQLite nativa en tiempo de ejecución, pero AOT la ha recortado o no sabe cómo incluirla correctamente.

### La pesadilla de dependencia transitoria

Peor aún, si tienes otros paquetes NuGet que usan SQLite (como algunas bibliotecas ORM o mi `mostlylucid.ephemeral.complete` paquete), puede terminar con **múltiples proveedores SQLite incompatibles** en tu árbol de dependencia.

Cada proveedor trata de trabajar de manera diferente:

- Uno podría esperar SQLite proporcionado por el sistema operativo (sólo Windows)
- Otro podría incluir su propia SQLite
- Otro podría usar una versión diferente de la biblioteca nativa

El compilador AOT se confunde sobre cuál usar, y a menudo el resultado es que no incluye ninguno de ellos, o peor aún, incluye archivos conflictivos que no pueden trabajar juntos.

## La solución: Utilice el paquete

Después de horas de frustración, encontré la solución: `SQLitePCLRaw.bundle_e_sqlite3`. Este es un paquete especial de NuGet diseñado específicamente para trabajar con AOT.

Añada estos dos paquetes a su proyecto:

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

### ¿Qué hace el paquete?

Los `bundle_e_sqlite3` el paquete es diferente de los proveedores SQLite normales:

**1. Incluye bibliotecas SQLite nativas precompiladas para cada plataforma importante:**

- Ventanas (x64, x86, ARM64)
- Linux (x64, ARM64, distros basados en muslo como Alpine)
- macOS (x64 Intel, ARM64 Apple Silicon)

**2. Estas bibliotecas nativas se empaquetan de una manera que el compilador AOT entiende**

El paquete marca explícitamente sus dependencias nativas para que el compilador AOT sepa incluirlas en el binario final. No hay carga dinámica, no hay tiempo de ejecución en busca de archivos.

**3. Está diseñado para construcciones multiplataforma**

Un paquete funciona para todas las plataformas. No necesita paquetes específicos de la plataforma o referencias condicionales.

### ¿Por qué funciona esto?

¿Recuerdas los dos problemas que identificamos?

**Problema 1: El recorte de AOT elimina bibliotecas que no puede ver siendo utilizadas**

- Solución: El paquete declara explícitamente sus bibliotecas nativas como activos de construcción que deben ser incluidos

**Problema 2: AOT no soporta carga dinámica**

- Solución: El paquete enlaza estáticamente las bibliotecas SQLite en tiempo de compilación

El resultado: cuando se construye para Windows x64, el paquete incluye `e_sqlite3.dll`. Cuando se construye para Linux ARM64, incluye el ARM64 `libe_sqlite3.so`Todo funciona.

### Critical: Inicializar el paquete (¡No te saltes esto!)

Esta es la parte que sube el 90% de la gente tratando de usar SQLite con AOT, incluyéndome en mi primer intento.

**Con .NET normal**, el paquete SQLite se inicializa automáticamente a sí mismo la primera vez que utiliza SQLite. La magia sucede detrás de las escenas, no es necesario hacer nada.

**Con AOT nativo**, esta inicialización automática no funciona. El compilador AOT no puede ver el código de inicio automático (parece código no utilizado y se recorta), por lo que **debe inicializar manualmente** el paquete al inicio de su aplicación.

Aquí está la línea mágica que necesitas:

```csharp
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();
    }
}
```

### ¿Qué hace? `Batteries.Init()` ¿Hacer?

Este método le dice al paquete SQLite que:

1. Encuentre la biblioteca SQLite nativa correcta para su plataforma actual
2. Carguelo en la memoria
3. Enchufe todas las conexiones entre `Microsoft.Data.Sqlite` y la biblioteca nativa

Se llama "Batteries" porque es el paquete de "baterías incluidas", todo lo que necesitas está empaquetado.

### Dónde ponerlo

Poner `Batteries.Init()` como **la primera línea** en tu `Main` método, antes de:

- Crear el constructor de aplicaciones
- Configuración de la inyección de dependencia
- Abrir cualquier conexión de base de datos
- Leyendo archivos de configuración que podrían usar SQLite

Piense en ello como conectar un dispositivo antes de intentar encenderlo. Si intenta utilizar SQLite antes de llamar `Init()`, todavía tendrás el `DllNotFoundException` a pesar de que el DLL está correctamente incluido en su aplicación.

### ¿Qué sucede si se olvida?

Si te olvidas de llamar `Batteries.Init()`, tu aplicación:

1. Construir con éxito (el compilador no le advertirá)
2. Empieza a correr
3. Choque el momento en que trata de utilizar SQLite con:

```
DllNotFoundException: Unable to load DLL 'e_sqlite3' or one of its dependencies
```

Esto es confuso porque el DLL **es** incluido en su solicitud — simplemente no está inicializado. Perdí dos horas por este error. No seas como yo.

## Configuración completa del proyecto

Aquí hay un lleno `.csproj` configurado para AOT nativo multiplataforma con SQLite:

```xml
<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>
```

### Ajustes clave explicados (en inglés sencillo)

Permítanme explicar lo que cada uno de estos ajustes hace:

**`PublishAot=true`**

Este es el switch maestro que permite la compilación Native AOT. Sin esto, se obtiene un comportamiento .NET normal (compilación JIT). Con esto, se obtiene con anticipación el código nativo compilado.

**`PublishTrimmed=true` y `TrimMode=full`**

Estos le dicen al compilador que elimine cualquier código que no esté usando. Piense en ello como limpiar su garaje antes de moverse — ¿por qué empacar cosas que no necesita?

- `PublishTrimmed=true` permite recortar
- `TrimMode=full` significa "ser agresivo, quitar todo lo que puedas"

**Aviso**: Esto puede romper el código que utiliza la reflexión pesada (como algunos seriadores JSON o ORMs) porque el recortador no siempre puede ver lo que estás usando a través de la reflexión.

**`InvariantGlobalization=true`**

Esto elimina todos los datos específicos de la cultura de su aplicación: formatos de fecha, símbolos de moneda, reglas de clasificación de texto para diferentes idiomas.

Sólo establecer esto a `true` si tu aplicación:

- Sólo usa el inglés
- No necesita un formato específico de fecha/hora
- Sólo utiliza comparaciones de cadenas ordinales (byte por byte)

Si está construyendo una herramienta CLI o una pasarela API que no le importa la localización, esto es un ahorro gratuito. Si está construyendo algo que necesita formatear fechas para los usuarios franceses o ordenar correctamente el texto turco, omita esta configuración.

**`PublishSingleFile=true`**

Agrupa todo en un archivo ejecutable. En lugar de tener:

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

Usted consigue justo:

```
myapp.exe
```

Mucho más fácil de desplegar.

**`StripSymbols=true`**

Los símbolos de depuración ayudan a los depuradores a mostrar nombres variables y números de línea cuando depuran. Son útiles durante el desarrollo, pero añaden varios megabytes a su binario final.

Esta configuración los elimina. Tu aplicación se ejecuta exactamente igual, sólo que más pequeño.

**`OptimizationPreference=Speed`**

Esto le dice al compilador qué priorizar al tomar decisiones:

- `Speed`: Hacerlo rápido (binarios ligeramente más grandes, pero mejor rendimiento)
- `Size`: Hacerlo pequeño (levemente más lento, pero tamaño binario mínimo)

Para la mayoría de las aplicaciones, `Speed` La diferencia de tamaño es generalmente sólo 2-3MB, pero la diferencia de rendimiento puede ser notable.

**`RuntimeIdentifiers`**

Esto declara qué plataformas quieres apoyar. No las construye todas — simplemente le dice a las herramientas "estos son objetivos válidos".

Identificadores disponibles:

- `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 (macs antiguos)
- `osx-arm64`: macOS Apple Silicon (M1/M2/M3 Macs)

Construye una plataforma a la vez usando `dotnet publish -r linux-x64`, por ejemplo.

## Construcción para múltiples plataformas

¿Recuerdas cómo dije que AOT requiere construcciones específicas de la plataforma? Necesitas compilar por separado para Windows, Linux x64, Linux ARM64, macOS Intel y macOS Apple Silicon. Hacer esto manualmente sería tedioso, pero podemos automatizarlo.

### ¿Qué es GitHub Actions?

Si no estás familiarizado, GitHub Actions es un servicio gratuito CI/CD (Integración continua/Desplegamiento continuo) integrado en GitHub. Te permite ejecutar tareas automatizadas cada vez que presionas código o creas una etiqueta de liberación.

Piensa en ello como tener un servidor de compilación que:

1. Observa tu repositorio GitHub
2. Cuando empujas una etiqueta como `v1.0.0`
3. Hace girar automáticamente máquinas virtuales de Windows, Linux y macOS
4. Crea tu app para todas las plataformas en paralelo
5. Crea una versión de GitHub con todos los binarios adjuntos

Todo esto se ejecuta en los servidores de GitHub, no es necesario mantener ninguna infraestructura. Para proyectos de código abierto y pequeños proyectos personales, es completamente gratuito.

### El flujo de trabajo completo de construcción

Aquí está un flujo de trabajo GitHub Actions que se construye automáticamente para todas las plataformas principales:

```yaml
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
```

### Cómo funciona este flujo de trabajo (paso a paso)

Si el YAML parece intimidante, esto es lo que hace en inglés:

**1. Activador (`on: push: tags`)**

El flujo de trabajo se ejecuta cuando pulsas una etiqueta Git que comienza con `v` (como `v1.0.0`, `v2.3.1`Esta es la forma estándar de marcar las versiones de lanzamiento.

**2. Estrategia Matriz**

Esta es la parte inteligente. En lugar de escribir cinco flujos de trabajo separados, definimos un **matriz** de construcciones:

- Cada construcción obtiene una diferente `os` (máquina de correr) y `runtime` (plataforma objetivo)
- GitHub Actions ejecuta las cinco construcciones **en paralelo**
- Cada uno produce un artefacto (el binario compilado)

Así que cuando empujas `v1.0.0`, GitHub simultáneamente:

- Genera una máquina Ubuntu para construir Linux x64 y ARM64
- Hace girar una máquina de Windows para construir Windows x64
- Hace girar una máquina macOS para construir macOS x64 y ARM64

**3. Pasos en cada edificio**

Cada construcción de plataforma hace los mismos pasos:

1. **Código de salida**: Obtiene su código fuente del repositorio
2. **Configuración .NET**: Instala .NET 9 SDK
3. **Instalar herramientas ARM64** (Linux ARM64 únicamente): instala herramientas de compilación cruzada
4. **Publicar**: Corre `dotnet publish` con banderas AOT para esa plataforma específica
5. **Cargar artefacto**: Guarda el binario compilado para que el próximo trabajo pueda acceder a él

**4. El resultado**

Después de completar todas las construcciones, tiene cinco artefactos (binarios) listos para distribuir. Puede descargarlos de la ejecución Acciones, o utilizar un segundo trabajo para crear una versión GitHub automáticamente (no se muestra en este fragmento, pero es fácil de añadir).

### Crítica: Las bibliotecas nativas no se agrupan

Aquí hay algo que me confundió durante horas: **`PublishSingleFile=true` sólo paquetes .NET código**Bibliotecas nativas como SQLite's `e_sqlite3.dll` Mantente separado.

Es por eso que el paso "Crear paquete de distribución" es tan importante:

```bash
# 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
```

Los `2>/dev/null || true` part significa "si no hay archivos que coincidan con este patrón, no fail—justo continúe". Esto permite que el mismo script funcione en todas las plataformas.

**Lo que usted distribuye:**

- Ventanas: `myapp.exe` + `e_sqlite3.dll` (agrupados en un ZIP)
- Linux: `myapp` + `libe_sqlite3.so` (agrupados en un tar.gz)
- macOS: `myapp` + `libe_sqlite3.dylib` (agrupados en un tar.gz)

Los usuarios extraen el archivo y ejecutan el ejecutable. La biblioteca SQLite nativa se encuentra junto al ejecutable, y el paquete lo encuentra automáticamente en tiempo de ejecución.

### Por qué la activación manual es útil

Nótese que `workflow_dispatch` desencadenar en la parte superior:

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

Esto le permite activar manualmente las construcciones desde la interfaz web de GitHub sin empujar una etiqueta. Útil para probar el flujo de trabajo o crear versiones beta.

### ARM64 Compilación cruzada

La compilación de Linux ARM64 requiere una atención especial. Necesita herramientas de compilación cruzada y debe especificar la correcta `objcopy` herramienta:

```bash
# 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
```

Sin la `ObjCopyName` parámetro, el linker falla con errores crípticos sobre formatos de archivo no reconocidos.

## Resultados en el mundo real

Esto es lo que he logrado con mi puerta de detección de bot basada en YARP de producción con middleware y SQLite loging. Este es un proyecto real que puede [descarga desde GitHub](https://github.com/scottgal/mostlylucid.nugetpackages/tree/main/Mostlylucid.BotDetection.Console).

### Tamaños binarios (de versiones reales de GitHub)

Estos son los **Tamaños de archivos reales** de mis versiones de GitHub, no estimaciones teóricas:

Plataforma  Ejecutable  SQLite Native DLL  Tamaño total del archivo
|----------|-----------|-------------------|-------------------|
Windows x64  9.2MB  +1.7MB **10,9MB** (ZIP)
Linux x64 10,8MB +1,6MB **12,4 MB** (tar.gz)
Linux ARM64  9.9MB  +1.5MB **11.4MB** (tar.gz)
macOS Intel  11.2MB  +1.8MB **13,0MB** (tar.gz)
macOS ARM64  9.8MB  +1.7MB **11,5 MB** (tar.gz)

Compárese esto con los despliegues autónomos de .NET 9 en **130-150MB por plataforma**Estamos hablando de un **10-12x reducción de tamaño**.

El desglose:

- Ejecución principal: Su código compilado + tiempo de ejecución .NET (compilado por AOT)
- DLL: El motor de base de datos SQLite (escrito en C)

Ambos archivos deben ser distribuidos juntos, pero todavía son dramáticamente más pequeños que las implementaciones tradicionales de .NET.

### Rendimiento de inicio

Inicio frío (primera solicitud servida) en un VPS de Linux modesto:

- **Self-contained .NET**: ~800ms
- **Nativo AOT**: ~150ms
- **Mejora**: 81% más rápido

Esto importa enormemente para:

- Funciones sin servidor/Lambda (se paga por milisegundo)
- Contenedores que aumentan o bajan con frecuencia
- Herramientas CLI donde cada invocación comienza de nuevo

### Uso de memoria

Memoria inactiva (corredor de la puerta, sin tráfico):

- **Self-contained .NET**: 85MB
- **Nativo AOT**: 42MB
- **Mejora**: 51% de reducción

Bajo carga (1000 solicitudes/segundo):

- **Self-contained .NET**: ~320MB
- **Nativo AOT**: ~180MB
- **Mejora**: 44% de reducción

La memoria inferior significa:

- Más contenedores por host
- Facturas de alojamiento en la nube más baratas
- Viabilidad en dispositivos con limitaciones de recursos (Raspberry Pi, IoT)

## Cascadas comunes

### 1. Olvidar `Batteries.Init()`

Este es el error #1. Incluso con el paquete correctamente referenciado, olvidando llamar `SQLitePCL.Batteries.Init()` en el mismo comienzo de su `Main` el método causará fallos en el tiempo de ejecución.

**La solución:**

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

### 2. No distribuir bibliotecas nativas

Esto es el error #2, y me dio dos veces. `PublishSingleFile=true` NO incluye DLLs SQLite nativos en su ejecutable. Debe distribuirlos junto con su ejecutable.

**¿Qué sucede si olvida:**

- Tu aplicación se construye bien
- Funciona bien en su máquina de desarrollo (donde SQLite ya podría estar instalado)
- Se estrella en la producción con `DllNotFoundException`

**La solución:**

Al empaquetar su liberación, siempre incluya:

```
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)
```

En tus scripts de implementación o acciones de GitHub:

```bash
# 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
```

Los usuarios deben extraer el archivo y ejecutar el ejecutable. La biblioteca nativa necesita estar en el mismo directorio.

### 3. Utilización `winsqlite3` Proveedor

Es posible que vea recomendaciones para usar `SQLitePCLRaw.provider.winsqlite3` en Windows para usar el SQLite proporcionado por el sistema operativo. No. Sólo funciona en Windows, requiere inicialización manual, y rompe las construcciones multiplataforma. `bundle_e_sqlite3`.

### 4. Recortar las advertencias con el núcleo de EF

Si está usando Entity Framework Core con SQLite, obtendrá advertencias de recorte (IL2026, IL3050). Estos son generalmente seguros de suprimir para el proveedor de SQLite de EF Core, pero prueba a fondo:

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

Mejor aún, considere usar Dapper o RAW ADO.NET con SQLite para aplicaciones AOT: son más amigables con AOT.

### 5. Faltan herramientas de Visual Studio (Windows)

En Windows, Native AOT requiere el enlace MSVC de Visual Studio. Si construye fuera de un programa de comandos de desarrolladores, obtendrá errores sobre `vswhere.exe`. GitHub Actions maneja esto automáticamente, pero para las construcciones locales, use:

- Prompt de comando de desarrollador para VS 2022
- Desarrollador PowerShell para VS 2022

O inicialice el entorno en su script de compilación:

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

## Cuándo usar el AOT nativo

El AOT nativo es perfecto para:

- **Herramientas CLI**: Asuntos de inicio instantáneo
- **Microservicios y contenedores**: Imágenes más pequeñas, escalar más rápido
- **Dispositivos de borde**: Raspberry Pi, dispositivos IoT con recursos limitados
- **Sin servidor/Lambda**: El tiempo de inicio frío es crítico
- **Aplicaciones Gateway/proxy**: Alto rendimiento, bajos gastos generales

Evitar el AOT para:

- Entity Framework Uso básico (muchas migraciones, consultas complejas)
- Aplicaciones reflexivas y pesadas
- Sistemas de complementos dinámicos
- Aplicaciones que necesitan generación de código de tiempo de ejecución

## Lista de comprobación de inicio rápido

Si has leído todo este post y sólo quieres una lista de verificación para seguir, aquí tienes:

**1. Añadir los paquetes:**

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

**2. Configure su `.csproj` en el caso de AOT:**

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

**3. Inicialice SQLite al inicio de su `Main` método:**

```csharp
SQLitePCL.Batteries.Init();
```

**4. Construir para su plataforma de destino:**

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

**5. ¡Pruebe su binario—sólo debe ejecutarse sin dependencias!**

## Conclusión

Hacer que SQLite trabaje con el nativo AOT no es obvio, pero una vez que conozcas el encantamiento mágico`SQLitePCLRaw.bundle_e_sqlite3` + `Batteries.Init()`La rentabilidad es sustancial: pequeños binarios, puesta en marcha instantánea y la capacidad de desplegarse en cualquier plataforma sin dependencias de tiempo de ejecución.

Para cualquiera que construya herramientas CLI, gateways o aplicaciones de borde con .NET, Native AOT con SQLite es ahora una opción realista. El flujo de trabajo de GitHub Actions proporcionado aquí automatiza todo el proceso de compilación multiplataforma, solo etiqueta una versión y listo.

Si eres nuevo en AOT, inicia pequeño: convierte una herramienta o utilidad CLI simple primero. Ponte cómodo con el proceso de compilación, aprende qué advertencias esperar, y entiende las limitaciones. Una vez que tengas lo básico abajo, puedes abordar aplicaciones más complejas.

El ecosistema .NET es cada vez más amigable con AOT. La mayoría de las bibliotecas modernas trabajan fuera de la caja o tienen documentación clara sobre el soporte AOT. El futuro es nativo, y es más rápido de lo que crees.

## Experiencia de despliegue real

Implemento mi puerta de entrada compilada por AOT a:

- **Gota Oceánica Digital** (Linux x64) - funciona como un servicio systemd
- **Frambuesa Pi 5** (Linux ARM64) - despliegue de borde para pruebas
- **Servidor de Windows** (Windows x64) - se ejecuta como un servicio de Windows
- **Contenedores Docker** - ambas variantes x64 y ARM64

El proceso de despliegue es idéntico en todas partes:

1. Descargar el archivo desde las versiones de GitHub
2. Extraerlo
3. Ejecutar el ejecutable

No hay "instalar .NET Runtime" paso. No hay infierno de dependencia. No hay conflictos de versión. Sólo extraer y ejecutar.

Por Docker, mi `Dockerfile` es embarazosamente simple:

```dockerfile
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"]
```

La imagen resultante es **~130MB** (principalmente la imagen base de Debian). Un contenedor .NET tradicional sería de 200-250MB.

## Ejemplo de trabajo completo

Todo lo que he mostrado aquí viene de un proyecto real, listo para la producción.

- **Ver el código fuente**: [Mayormentelucid.BotDetection.Consola](https://github.com/scottgal/mostlylucid.nugetpackages/tree/main/Mostlylucid.BotDetection.Console)
- **Vea el flujo de trabajo de las acciones de GitHub**: [consola-gateway-release.yml](https://github.com/scottgal/mostlylucid.nugetpackages/blob/main/.github/workflows/console-gateway-release.yml)
- **Descargar binarios preconstruidos**: [Liberaciones de GitHub](https://github.com/scottgal/mostlylucid.nugetpackages/releases)

El proyecto es un proxy reverso YARP mínimo con middleware de detección de bots que registra firmas a SQLite. Demuestra:

- AOT nativo con SQLite
- Multiplataforma se construye a través de acciones GitHub
- Agrupación de la biblioteca nativa
- Análisis del argumento CLI
- Registro estructurado
- Agraciado cierre

Clonarlo, estudiarlo, utilizarlo como una plantilla para sus propios proyectos AOT.

## Recursos

- [Documentación oficial .NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
- [Repositorio SQLitePCLRaw GitHub](https://github.com/ericsink/SQLitePCL.raw)
- [Ejemplo de trabajo: Gateway de detección de bots de la mayoría de los lúcidos](https://github.com/scottgal/mostlylucid.nugetpackages/tree/main/Mostlylucid.BotDetection.Console)
- [Mi flujo de trabajo de acciones de GitHub](https://github.com/scottgal/mostlylucid.nugetpackages/blob/main/.github/workflows/console-gateway-release.yml)

Ahora ve a construir algo pequeño y rápido.