Skip to content
← Alle Beiträge
· Delta1 Labs Nebula.NETObfuskierung.NETLeitfaden

Eine Entity-Framework-Core-App obfuskieren, ohne die Datenschicht zu zerbrechen

EF Core bildet deine CLR-Typen anhand ihrer Namen auf Tabellen ab. Benenne eine Entität oder eine Eigenschaft mit einem Obfuskator um, und standardmäßig wandert das von EF erwartete Schema mit — dein DbContext sucht nun eine Tabelle namens "a" mit einer Spalte "c", Migrationen passen nicht mehr und zeichenkettenbasierter Eigenschaftszugriff wirft zur Laufzeit. Die Lösung ist nicht, deine gesamte Datenschicht von der Obfuskierung auszunehmen. Sie besteht darin, genau zu verstehen, welche Namen EF zur Laufzeit liest, dein Mapping explizit zu machen, um das Schema von den CLR-Namen zu entkoppeln, und Nebula.NET dann die Member frei umbenennen zu lassen. Hier ist das vollständige Bild, mit den Umbenennungsregeln und den EF-Mustern, die es sicher machen.

Entity Framework Core ist eine Mapping-Schicht, und das, woraus es standardmäßig abbildet, sind deine Namen. Nenne eine Klasse Invoice mit einer Eigenschaft Status und — ganz ohne Konfiguration — entscheidet EF, dass es eine Tabelle Invoices mit einer Spalte Status gibt, weil die Konvention das Schema aus den CLR-Bezeichnern ableitet. Diese Konvention ist wunderbar für die Produktivität und eine Mine für die Obfuskierung. In dem Moment, in dem Nebula.NET Invoice in a und Status in b umbenennt, folgert die Konvention getreulich, dass deine Datenbank eine Tabelle a mit einer Spalte b hat, und nichts stimmt mehr mit der Realität überein.

Die übliche Reaktion ist, die gesamte Datenschicht von der Obfuskierung auszunehmen. Das funktioniert und ist ein schlechter Handel: Deine Entitäten und dein DbContext sind oft die klarste Beschreibung deiner Domäne, die du auslieferst, und du hast sie gerade im Klartext gelassen, um ein Datenbankschema zu schützen, das nie der sensible Teil war. Stattdessen gibt es eine präzise Lösung. Finde jede Stelle, an der EF zur Laufzeit einen CLR-Namen liest, mache das Schema explizit, damit es aufhört, aus diesen Namen abzuleiten, und lass Nebula dann die Member umbenennen. Dieser Beitrag ist diese Bestandsaufnahme und diese Muster.

Wo EF tatsächlich einen Namen liest

Umbenennen ist genau dort sicher, wo EF zur Laufzeit nicht vom ursprünglichen Bezeichner abhängt, und unsicher, wo es das tut. Es gibt vier Stellen, an denen es das tut:

  1. Konventionsbasierte Tabellen- und Spaltennamen. Ohne explizites Mapping ist der Tabellenname der Name der DbSet-Eigenschaft (oder der Typname) und jede Spalte ist der Eigenschaftsname. Benenne den Member um, und der abgeleitete Schemaname wandert mit.
  2. Zeichenkettenbasierter Memberzugriff. EF.Property<T>("Status"), entry.Property("Status") und Schatten-Eigenschafts-Lookups lösen einen Member per Zeichenkette auf. Eine Zeichenkette nimmt an der Umbenennung nicht teil, also nennt sie nach der Obfuskierung einen Member, der nicht mehr existiert, und wirft.
  3. Migrationen. Eine generierte Migration und der Modell-Snapshot enthalten die Schemanamen als Zeichenketten-Literale. Wurden sie per Konvention erzeugt, kodieren sie die ursprünglichen Namen; nach einer Umbenennung leitet das laufende Modell andere Namen ab und die beiden stimmen nicht überein.
  4. Alles, was über dein Modell per Name reflektiert — einige Serialisierer, Audit-Zwischenschichten oder eine Provider-Funktion, die sich auf Namen von Navigationseigenschaften stützt.

LINQ steht bemerkenswerterweise nicht auf dieser Liste. Where(i => i.Status == open) kompiliert zu einem Ausdrucksbaum, der ein MemberInfo enthält, nicht die Zeichenkette "Status". Wenn Nebula die Eigenschaft umbenennt, zeigt dieses MemberInfo auf den umbenannten Member, und EF übersetzt es über dein Mapping. Konsistente Umbenennung — die Nebula über das gesamte Assembly garantiert — hält jede kompilierte Abfrage intakt. Der Bruch geht immer um aus Zeichenketten aufgelöste Namen, nie um typisierten Memberzugriff.

Konvention: Schema aus CLR-Name abgeleitetStatus → (umben.) bleitet abSpalte "b" ✗ kein TrefferDB hat weiter Spalte "Status"Explizit: HasColumnName nagelt das Schema festStatus → (umben.) bfreie Umben.HasColumnName("Status")Spalte "Status" ✓Das feste Zeichenketten-Literal wird von Nebula verschlüsselt, aber nie umbenannt — Membername und Spaltenname sind entkoppelt.

Nagle das Schema fest, dann obfuskiere frei

Die ganze Strategie ist eine Idee: den Schemanamen vom CLR-Namen entkoppeln. Sobald jeder Tabellen-, Spalten-, Schlüssel- und Indexname ein Zeichenketten-Literal in deiner Konfiguration ist, tragen die CLR-Bezeichner keine Schemabedeutung mehr, und Nebula kann sie ohne Wirkung auf die Datenbank in a.b umbenennen.

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");
    });
}

Diese Zeichenketten-Literale — "Invoices", "Status", "IX_Invoices_Status" — sind genau die Art Sache, die Nebulas Zeichenkettenverschlüsselung in der Ausgabe schützt, aber sie werden nie umbenannt, weil sie Daten sind, keine Bezeichner. Die Lambdas x => x.Status sind typisierter Memberzugriff, also folgen sie der Umbenennung automatisch. Das Nettoergebnis: Invoice.Status kann im ausgelieferten Binary zu a.b werden, während das von EF ausgegebene SQL weiterhin die Spalte Status liest und schreibt.

Datenannotationen erreichen dieselbe Entkopplung, wenn du sie am Typ bevorzugst:

[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; }
}

In beiden Fällen lautet die Obfuskierungsregel in nebula.json schlicht, dass deine Datenschicht nicht besonders ist: Sie wird mit allem anderen obfuskiert:

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

Keine pauschale Ausnahme für MyApp.Data.*. Die Entitäten werden umbenannt; das Schema ist in der Konfiguration festgenagelt; die beiden berühren sich nicht mehr.

Beseitige den zeichenkettenbasierten Zugriff

Die Muster, die wirklich brechen, sind jene, die einen Member aus einer Zeichenkette auflösen, denn eine Zeichenkette folgt nie einer Umbenennung. Finde sie und wechsle zur typisierten Form.

// ✗ 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);

Schatten-Eigenschaften sind der einzige Fall, in dem eine Zeichenkette legitim ist — der Member existiert auf dem CLR-Typ gar nicht —, also gibt es nichts umzubenennen und nichts zu brechen. Deklariere sie explizit und greife über ihren Schema-Namen darauf zu, den du kontrollierst:

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

Rohes SQL ist die andere zu prüfende Stelle. Eine rohe Abfrage muss die Spalte (Status) nennen, nie die CLR-Eigenschaft — und da du den Spaltennamen auf ein Literal festgenagelt hast, stimmen diese dauerhaft überein, unabhängig von der Obfuskierung:

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

Migrationen: gegen das gemappte Modell generieren

Migrationen kodieren Schemanamen als Literale, also sind sie sicher, wenn diese Literale deine festgenagelten Namen sind. Sie werden nur dann zur Gefahr, wenn der Snapshot per Konvention generiert wurde und daher CLR-abgeleitete Namen erfasst hat, die sich später verschieben. Zwei Regeln halten Migrationen sauber:

  • Generiere Migrationen aus dem nicht obfuskierten Build. Das Entwurfszeit-Tooling reflektiert über dein Modell, um die Migration und den Snapshot zu erzeugen; führe es gegen das normale Debug-Assembly aus, nie gegen das obfuskierte Artefakt. Die Migration, die du commitest, enthält dann deine expliziten Schema-Literale.
  • Setze nie nameof(SomeProperty) in eine handbearbeitete Migration. nameof erfasst den CLR-Namen zur Kompilierzeit, also gibt es nach einer Umbenennung "b" aus, wo du die Spalte "Status" meintest. Schreibe den Spaltennamen als 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)

Da das obfuskierte Binary und die Migration beide dasselbe feste Schema-Vokabular sprechen, verhält sich context.Database.Migrate() beim Start identisch, ob das Assembly obfuskiert wurde oder nicht.

Die seltene chirurgische Ausnahme

Gelegentlich reflektiert eine bestimmte Funktion zur Laufzeit doch über einen CLR-Namen — eine Audit-Komponente, die den Namen der Navigationseigenschaft aufzeichnet, oder eine Provider-Erweiterung, die sich darauf stützt. Wenn das passiert, bewahre diesen Member, nicht das Assembly. Nebula erlaubt dir, einen einzelnen Member mit einem Attribut auszunehmen, was die Ausnahme im Code sichtbar hält, wo die Abhängigkeit lebt:

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

Das ist die ganze Notluke — eine Eigenschaft, durch ihr eigenes Attribut dokumentiert — statt eines Platzhalters, der dein gesamtes Domänenmodell verschonen würde. Die Disziplin lohnt sich: Der Sinn, eine datengetriebene App zu obfuskieren, ist, dass die wertvolle Logik in den Entitäten und der Abfrageschicht steckt, und eine pauschale Ausnahme schützt davon nichts.

Zusammengefügt

EF Core bricht unter Obfuskierung nur dort, wo es zur Laufzeit einen Namen aus einer Zeichenkette auflöst, und jede dieser Stellen hat eine entkoppelte Alternative. Nagle Tabellen, Spalten, Schlüssel und Indizes mit explizitem Mapping fest, damit das Schema aufhört, aus CLR-Bezeichnern abzuleiten; ersetze EF.Property und nameof-in-Migrationen durch die typisierte oder die Literal-Form; generiere Migrationen aus dem sauberen Build. Tu das, und deine MyApp.Data.*-Typen werden neben dem Rest der App obfuskiert — umbenannt, zeichenkettenverschlüsselt, kontrollflussgehärtet —, während die Datenbank genau das Schema sieht, das sie immer gesehen hat. Du schützt das Domänenmodell, das es zu schützen galt, und gibst nichts an die Mapping-Schicht ab.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.