Zum Inhalt springen

C# LINQ für Datenbankabfragen: Vergleich mit Doctrine DQL und Entity Framework Core

Veröffentlicht am 22. Sept. 2025 | ca. 5 Min. Lesezeit |

LINQ (Language Integrated Query) ist eine der bemerkenswertesten Eigenschaften von C#. Es ermöglicht, Abfragen in Collections, XML, APIs und Datenbanken mit einheitlicher Syntax direkt im Code zu schreiben — typsicher, mit IDE-Autocompletion und Compile-Zeit-Prüfung. In Kombination mit Entity Framework Core (EF Core) ist es das direkte Äquivalent zu Symfony's Doctrine ORM mit DQL.

LINQ-Grundlagen: Collections abfragen

LINQ funktioniert mit jeder IEnumerable<T>:

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Method Syntax (empfohlen)
var evenSquares = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * n)
    .OrderBy(n => n)
    .ToList();
// Ergebnis: [4, 16, 36, 64, 100]

// Query Syntax (SQL-ähnlich, weniger gebräuchlich)
var evenSquaresQuery =
    from n in numbers
    where n % 2 == 0
    orderby n * n
    select n * n;

Vergleich mit PHP

// PHP mit array_filter / array_map
$evenSquares = array_map(
    fn(int $n): int => $n * $n,
    array_filter($numbers, fn(int $n): bool => $n % 2 === 0)
);
sort($evenSquares);

// PHP mit LINQ-ähnlicher Collection-Library (z.B. Doctrine ArrayCollection)
$collection->filter(fn($n) => $n % 2 === 0)->map(fn($n) => $n * $n);

C# LINQ ist typsicher und lazy: Die Abfrage wird erst ausgewertet, wenn man .ToList(), .ToArray() oder über die Sequenz iteriert.

Entity Framework Core Setup

EF Core ist das Doctrine-Äquivalent im .NET-Ökosystem: Code-First ORM mit Migrations.

// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string Category { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }

    public int? CategoryId { get; set; }
    public Category? CategoryNavigation { get; set; }
}

// Data/AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Name).HasMaxLength(255).IsRequired();
            entity.Property(p => p.Price).HasPrecision(10, 2);
            entity.HasOne(p => p.CategoryNavigation)
                  .WithMany()
                  .HasForeignKey(p => p.CategoryId);
        });
    }
}

LINQ mit Entity Framework Core — Datenbankabfragen

EF Core übersetzt LINQ-Ausdrücke in SQL. Das ist analog zu Doctrine's QueryBuilder oder DQL.

Einfache Abfragen

// Alle Produkte laden
List<Product> all = await context.Products.ToListAsync();

// Mit WHERE
List<Product> affordable = await context.Products
    .Where(p => p.Price < 100)
    .ToListAsync();

// Einzelnes Produkt
Product? product = await context.Products
    .FirstOrDefaultAsync(p => p.Id == 42);

Doctrine-Äquivalent:

// Alle Produkte
$all = $productRepository->findAll();

// Mit WHERE
// Hinweis: findBy() unterstützt nur Gleichheitsvergleiche, daher QueryBuilder:
$affordable = $em->createQueryBuilder()
    ->select('p')
    ->from(Product::class, 'p')
    ->where('p.price < :price')
    ->setParameter('price', 100)
    ->getQuery()
    ->getResult();

Komplexe Abfragen mit JOIN

// JOIN mit Navigation Properties
var productsWithCategory = await context.Products
    .Include(p => p.CategoryNavigation)
    .Where(p => p.CategoryNavigation!.Name == "Electronics")
    .OrderBy(p => p.Price)
    .Select(p => new
    {
        p.Name,
        p.Price,
        Category = p.CategoryNavigation!.Name
    })
    .ToListAsync();

Doctrine-Äquivalent:

$qb = $em->createQueryBuilder()
    ->select('p.name', 'p.price', 'c.name AS category')
    ->from(Product::class, 'p')
    ->join('p.category', 'c')
    ->where('c.name = :name')
    ->setParameter('name', 'Electronics')
    ->orderBy('p.price', 'ASC')
    ->getQuery()
    ->getResult();

Aggregation und Gruppierung

// GROUP BY mit Aggregation
var priceByCategory = await context.Products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        Count = g.Count(),
        AveragePrice = g.Average(p => p.Price),
        MinPrice = g.Min(p => p.Price),
        MaxPrice = g.Max(p => p.Price)
    })
    .OrderByDescending(r => r.Count)
    .ToListAsync();

Doctrine DQL:

SELECT p.category, COUNT(p.id) as cnt, AVG(p.price) as avgPrice
FROM App\Entity\Product p
GROUP BY p.category
ORDER BY cnt DESC

Paginierung

// Seite 2, 20 Elemente pro Seite
int page = 2;
int pageSize = 20;

var pagedProducts = await context.Products
    .OrderBy(p => p.Id)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();

// Gesamtanzahl für Pagination
int total = await context.Products.CountAsync();

Doctrine:

$query->setFirstResult(($page - 1) * $pageSize)
      ->setMaxResults($pageSize)
      ->getQuery()
      ->getResult();

Projections: Nur benötigte Felder laden

// DTO-Record für Projektion
public record ProductSummary(int Id, string Name, decimal Price);

// Projektion mit Select
var summaries = await context.Products
    .Where(p => p.Price > 50)
    .Select(p => new ProductSummary(p.Id, p.Name, p.Price))
    .ToListAsync();

EF Core übersetzt das in SELECT id, name, price FROM products WHERE price > 50 — nur die benötigten Spalten werden geladen.

Raw SQL für komplexe Abfragen

Wenn LINQ nicht ausreicht:

// Raw SQL mit EF Core
var products = await context.Products
    .FromSqlRaw("SELECT * FROM products WHERE MATCH(name, description) AGAINST ({0})", searchTerm)
    .ToListAsync();

// Raw SQL für Nicht-Entity-Abfragen (ab EF Core 8)
var results = await context.Database
    .SqlQuery<ProductSummary>($"SELECT id, name, price FROM products WHERE price > {minPrice}")
    .ToListAsync();

N+1 Problem vermeiden

Wie bei Doctrine gibt es das N+1-Problem, wenn Navigation Properties lazy geladen werden:

// SCHLECHT: N+1 Query
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
    // Für jedes Produkt eine separate Query!
    Console.WriteLine(product.CategoryNavigation?.Name);
}

// GUT: Eager Loading mit Include
var products = await context.Products
    .Include(p => p.CategoryNavigation)
    .ToListAsync();

Fazit

LINQ + EF Core ist das mächtigste ORM-Duo in der .NET-Welt. Für PHP-Entwickler, die Doctrine kennen, sind die Konzepte vertraut: Code-First, Migrations, Navigation Properties (Relationen), QueryBuilder-Äquivalent. Der Hauptvorteil von LINQ gegenüber DQL: alles ist typsicher, der Compiler erkennt Fehler sofort, und die IDE bietet vollständige Autocompletion.

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.