Skip to content
← Todas las publicaciones
· Delta1 Labs Nebula.NETOfuscación.NETGuía

Ofuscar una app de Entity Framework Core sin romper tu capa de datos

EF Core asigna tus tipos CLR a tablas por sus nombres. Renombra una entidad o una propiedad con un ofuscador y, por defecto, el esquema que EF espera se mueve con ellos: tu DbContext ahora busca una tabla llamada "a" con una columna "c", las migraciones dejan de coincidir y el acceso a propiedades basado en cadenas lanza en tiempo de ejecución. La solución no es excluir toda tu capa de datos de la ofuscación. Es entender exactamente qué nombres lee EF en tiempo de ejecución, hacer explícito tu mapeo para desacoplar el esquema de los nombres CLR y luego dejar que Nebula.NET renombre los miembros libremente. Aquí está el panorama completo, con las reglas de renombrado y los patrones de EF que lo hacen seguro.

Entity Framework Core es una capa de mapeo, y aquello desde lo que mapea, por defecto, son tus nombres. Llama a una clase Invoice con una propiedad Status y —sin ninguna configuración— EF decide que hay una tabla Invoices con una columna Status, porque la convención deriva el esquema de los identificadores CLR. Esa convención es maravillosa para la productividad y una mina para la ofuscación. En el momento en que Nebula.NET renombra Invoice a a y Status a b, la convención concluye fielmente que tu base de datos tiene una tabla a con una columna b, y ya nada coincide con la realidad.

La reacción habitual es excluir toda la capa de datos de la ofuscación. Eso funciona y es un mal canje: tus entidades y tu DbContext suelen ser la descripción más clara de tu dominio que entregas, y acabas de dejarlos en texto plano para proteger un esquema de base de datos que nunca fue la parte sensible. En su lugar hay un arreglo preciso. Encuentra cada lugar donde EF lee un nombre CLR en tiempo de ejecución, haz explícito el esquema para que deje de derivar de esos nombres y luego deja que Nebula renombre los miembros. Este artículo es ese inventario y esos patrones.

Dónde lee EF realmente un nombre

El renombrado es seguro exactamente donde EF no depende del identificador original en tiempo de ejecución, e inseguro donde sí. Hay cuatro lugares donde sí:

  1. Nombres de tabla y columna basados en convenciones. Sin mapeo explícito, el nombre de tabla es el nombre de la propiedad DbSet (o el nombre del tipo) y cada columna es el nombre de la propiedad. Renombra el miembro y el nombre de esquema derivado se mueve con él.
  2. Acceso a miembros basado en cadenas. EF.Property<T>("Status"), entry.Property("Status") y las búsquedas de propiedades sombra resuelven un miembro por cadena. Una cadena no participa en el renombrado, así que tras la ofuscación nombra un miembro que ya no existe y lanza.
  3. Migraciones. Una migración generada y la instantánea del modelo contienen los nombres de esquema como literales de cadena. Si se produjeron por convención, codifican los nombres originales; tras un renombrado el modelo en ejecución deriva nombres distintos y ambos no concuerdan.
  4. Cualquier cosa que use reflexión sobre tu modelo por nombre: unos pocos serializadores, capas de auditoría o una característica de proveedor que se apoye en nombres de propiedades de navegación.

LINQ, notablemente, no está en esta lista. Where(i => i.Status == open) compila a un árbol de expresión que contiene un MemberInfo, no la cadena "Status". Cuando Nebula renombra la propiedad, ese MemberInfo apunta al miembro renombrado, y EF lo traduce mediante tu mapeo. El renombrado coherente —que Nebula garantiza en todo el ensamblado— mantiene intacta cada consulta compilada. La ruptura siempre es sobre nombres resueltos desde cadenas, nunca sobre acceso tipado a miembros.

Convención: esquema derivado del nombre CLRStatus → (renombr.) bderivacolumna "b" ✗ sin matchla BD aún tiene columna "Status"Explícito: HasColumnName fija el esquemaStatus → (renombr.) brenombr. libreHasColumnName("Status")columna "Status" ✓El literal de cadena fijo lo cifra Nebula pero nunca lo renombra, así que el nombre del miembro y el de la columna quedan desacoplados.

Fija el esquema, luego ofusca libremente

Toda la estrategia es una idea: desacoplar el nombre de esquema del nombre CLR. Una vez que cada nombre de tabla, columna, clave e índice es un literal de cadena en tu configuración, los identificadores CLR no llevan ningún significado de esquema, y Nebula puede renombrarlos a a.b sin efecto sobre la base de datos.

protected override void OnModelCreating(ModelBuilder model)
{
    model.Entity<Invoice>(e =>
    {
        e.ToTable("Invoices");                 // table name is now a literal
        e.HasKey(x => x.Id);
        e.Property(x => x.Id).HasColumnName("Id");
        e.Property(x => x.Status).HasColumnName("Status");
        e.Property(x => x.Total).HasColumnName("Total").HasPrecision(18, 2);
        e.HasIndex(x => x.Status).HasDatabaseName("IX_Invoices_Status");

        // Navigation + FK column, pinned the same way.
        e.HasMany(x => x.Lines)
         .WithOne(l => l.Invoice)
         .HasForeignKey(l => l.InvoiceId)
         .HasConstraintName("FK_InvoiceLines_Invoices");
    });
}

Esos literales de cadena —"Invoices", "Status", "IX_Invoices_Status"— son exactamente el tipo de cosa que el cifrado de cadenas de Nebula protege en la salida, pero nunca se renombran, porque son datos, no identificadores. Las lambdas x => x.Status son acceso tipado a miembros, así que cabalgan sobre el renombrado automáticamente. El resultado neto: Invoice.Status puede convertirse en a.b en el binario entregado mientras que el SQL que EF emite sigue leyendo y escribiendo la columna Status.

Las anotaciones de datos logran el mismo desacoplamiento si las prefieres en el tipo:

[Table("Invoices")]
public class Invoice
{
    [Column("Id")]     public int Id { get; set; }
    [Column("Status")] public InvoiceStatus Status { get; set; }
    [Column("Total")]  public decimal Total { get; set; }
}

En cualquier caso, la regla de ofuscación en nebula.json es simplemente que tu capa de datos no es especial: se ofusca con todo lo demás:

{
  "preservePublicApi": true,
  "encryptStrings": true,
  "controlFlowObfuscation": true,
  "renaming": {
    "rules": [
      { "pattern": "MyApp.Data.*", "action": "rename" }
    ]
  }
}

Nada de exclusión general para MyApp.Data.*. Las entidades se renombran; el esquema queda fijado en la configuración; los dos ya no se tocan.

Elimina el acceso basado en cadenas

Los patrones que de verdad se rompen son los que resuelven un miembro desde una cadena, porque una cadena nunca sigue un renombrado. Encuéntralos y pasa a la forma tipada.

// ✗ Breaks: "Status" names a CLR member that obfuscation renamed to "b".
var q1 = db.Invoices.Where(i => EF.Property<InvoiceStatus>(i, "Status") == open);

// ✓ Typed member access — compiles to a MemberInfo that follows the rename.
var q2 = db.Invoices.Where(i => i.Status == open);

Las propiedades sombra son el único caso donde una cadena es legítima —el miembro no existe en absoluto en el tipo CLR—, así que no hay nada que renombrar ni nada que romper. Decláralas explícitamente y accede a ellas por su nombre de esquema, que tú controlas:

model.Entity<Invoice>().Property<DateTime>("LastModifiedUtc");   // shadow, no CLR member
// ...
var ts = db.Entry(invoice).Property("LastModifiedUtc").CurrentValue;  // safe: not a CLR name

El SQL crudo es el otro lugar que auditar. Una consulta cruda debe nombrar la columna (Status), nunca la propiedad CLR, y como fijaste el nombre de columna a un literal, esos coinciden permanentemente sin importar la ofuscación:

// Names the column, which is fixed — safe under any rename.
var rows = db.Invoices.FromSqlInterpolated(
    $"SELECT * FROM Invoices WHERE Status = {(int)open}");

Migraciones: genéralas contra el modelo mapeado

Las migraciones codifican los nombres de esquema como literales, así que son seguras si esos literales son tus nombres fijados. Se vuelven un peligro solo cuando la instantánea se generó por convención y por tanto capturó nombres derivados de CLR que luego se mueven. Dos reglas mantienen limpias las migraciones:

  • Genera las migraciones desde la compilación no ofuscada. El tooling de diseño usa reflexión sobre tu modelo para producir la migración y la instantánea; ejecútalo contra el ensamblado Debug normal, nunca contra el artefacto ofuscado. La migración que confirmas contiene entonces tus literales de esquema explícitos.
  • Nunca pongas nameof(SomeProperty) en una migración editada a mano. nameof captura el nombre CLR en tiempo de compilación, así que tras un renombrado emite "b" donde querías decir la columna "Status". Escribe el nombre de columna como literal.
// In a migration — literal column names, matching your explicit mapping.
migrationBuilder.CreateIndex(
    name: "IX_Invoices_Status",
    table: "Invoices",
    column: "Status");          // literal, not nameof(Invoice.Status)

Como el binario ofuscado y la migración hablan ambos con el mismo vocabulario de esquema fijo, context.Database.Migrate() al arrancar se comporta de forma idéntica haya sido o no ofuscado el ensamblado.

La rara exclusión quirúrgica

De vez en cuando una característica concreta aún usa reflexión sobre un nombre CLR en tiempo de ejecución: un componente de auditoría que registra el nombre de la propiedad de navegación, o una extensión de proveedor que se apoya en él. Cuando eso ocurre, preserva ese miembro, no el ensamblado. Nebula te permite excluir un solo miembro con un atributo, que mantiene visible la exclusión en el código donde vive la dependencia:

public class Invoice
{
    // A downstream component reflects over this navigation's name at runtime.
    [Obfuscation(Feature = "renaming", Exclude = true)]
    public List<InvoiceLine> Lines { get; set; } = new();
}

Esa es toda la vía de escape —una propiedad, documentada por su propio atributo— en lugar de un comodín que exima todo tu modelo de dominio. La disciplina vale la pena: el objetivo de ofuscar una app dirigida por datos es que la lógica valiosa está en las entidades y la capa de consultas, y una exclusión general no protege nada de eso.

Juntándolo todo

EF Core se rompe bajo la ofuscación solo donde resuelve un nombre desde una cadena en tiempo de ejecución, y cada uno de esos lugares tiene una alternativa desacoplada. Fija tablas, columnas, claves e índices con mapeo explícito para que el esquema deje de derivar de los identificadores CLR; reemplaza EF.Property y nameof-en-migraciones por las formas tipada o literal; genera las migraciones desde la compilación limpia. Haz eso y tus tipos MyApp.Data.* se ofuscan junto al resto de la app —renombrados, con cadenas cifradas, con flujo de control endurecido— mientras la base de datos ve exactamente el esquema que siempre ha visto. Proteges el modelo de dominio que valía la pena proteger y no cedes nada a la capa de mapeo.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.