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.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.