Entwicklung & Code
Neu in .NET 10.0 [24]: LINQ-Operatoren RightJoin() und LeftJoin() in EF Core
.NET 10.0 führt die LINQ-Operatoren LeftJoin() und RightJoin() ein, die auch in Entity Framework Core 10.0 mit allen Datenbankmanagementsystemen funktionieren: Der Object Relational Mapper (ORM) übersetzt sie in entsprechende SQL-Befehle, siehe die zugehörigen Issues zu LeftJoin und RightJoin.
Weiterlesen nach der Anzeige

Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.
Zuvor musste man einen Left (Outer) Join und Right (Outer) Join wie in LINQ-to-Objects umständlich mit GroupJoin() und SelectMany() sowie DefaultIfEmpty() bilden.
Einige Codebeispiele zeigen im Folgenden den Einsatz von RightJoin() und LeftJoin() bei Entity Framework Core 10.0 in Verbindung mit einer relationalen Datenbank. Auch in Entity Framework konnte man das Ergebnis von RightJoin() und LeftJoin() bisher schon erzielen via GroupJoin() und SelectMany() mit DefaultIfEmpty(). Die Beispiele beginnen mit der alten Variante und zeigen dann die neue Variante.
(Bild: King / stock.adobe.com)

Das ist neu in .NET 11.0: Dr. Holger Schwichtenberg und weitere Experten präsentieren am 17. November 2026 auf der Online-Konferenz betterCode() .NET 11.0 die Änderungen für Entwicklerinnen und Entwickler in .NET SDK, C# 15.0 und mehr. Bis zur Veröffentlichung des Programms sind vergünstigte Blind-Bird-Tickets verfügbar.
Folgender Code zeigt LeftJoin() in einer Datenbank mit den Tabellen „Flight“ und „Pilot“:
///
/// LeftJoin(): Suche alle Flüge, zu denen es keinen Piloten gibt
///
public void EFC10_LeftJoin()
{
CUI.Demo(nameof(EFC10_LeftJoin));
var ctx = new DA.WWWings.WwwingsV1EnContext();
#region --------------- ALT
CUI.H2("ALT: Suche alle Flüge, zu denen es keinen Piloten gibt via GroupJoin() und SelectMany()");
var fluegeOhnePilotAlt = ctx.Flights
.GroupJoin(
ctx.Pilots,
f => f.PilotPersonId,
p => p.PersonId,
(f, pilots) => new { Flight = f, Pilots = pilots.DefaultIfEmpty() }
)
.SelectMany(
fp => fp.Pilots,
(fp, p) => new
{
fp.Flight.FlightNo,
fp.Flight.Departure,
fp.Flight.Destination,
fp.Flight.FlightDate,
PilotId = fp.Flight.PilotPersonId == null ? "n/a" : fp.Flight.PilotPersonId.ToString(),
GivenName = p.Employee.Person.GivenName,
Surname = p.Employee.Person.Surname
}
)
.Where(x => x.Surname == null)
.Take(20)
.ToList();
Console.WriteLine("Gefundene Flüge: " + fluegeOhnePilotAlt.Count);
foreach (var item in fluegeOhnePilotAlt)
{
Console.WriteLine($"{item.FlightNo} {item.Departure}->{item.Destination} am {item.FlightDate}: Pilot {item.PilotId} {item.GivenName} {item.Surname}");
}
#endregion
#region --------------- NEU
CUI.H2("NEU: Suche alle Flüge, zu denen es keinen Piloten gibt via LeftJoin()");
var fluegeOhnePilotNeu = ctx.Flights
.LeftJoin(
ctx.Pilots,
f => f.PilotPersonId,
p => p.PersonId,
(f, p) => new
{
f.FlightNo,
f.Departure,
f.Destination,
f.FlightDate,
PilotId = f.PilotPersonId == null ? "n/a" : f.PilotPersonId.ToString(),
p.Employee.Person.GivenName,
p.Employee.Person.Surname,
}).Where(x => x.Surname == null).Take(20).ToList();
Console.WriteLine("Gefundene Flüge: " + fluegeOhnePilotNeu.Count);
foreach (var item in fluegeOhnePilotNeu)
{
Console.WriteLine($"{item.FlightNo} {item.Departure}->{item.Destination} am {item.FlightDate}: Pilot {item.PilotId} {item.GivenName} {item.Surname}");
}
#endregion
#region --------------- Kontrolle
CUI.H3("Zur Kontrolle:");
// Zur Kontrolle:
if (fluegeOhnePilotNeu.Count() > 0)
{
var f = ctx.Flights.Find(fluegeOhnePilotNeu[0].FlightNo);
Console.WriteLine(f.ToNameValueString());
}
#endregion
}
Aus diesem LINQ-Befehl mit LeftJoin() entsteht folgender SQL-Befehl:
Weiterlesen nach der Anzeige
SELECT TOP(@p) [f].[FlightNo], [f].[Departure], [f].[Destination], [f].[FlightDate], CASE
WHEN [f].[Pilot_PersonID] IS NULL THEN 'n/a'
ELSE COALESCE(CONVERT(varchar(11), [f].[Pilot_PersonID]), '')
END AS [PilotId], [p0].[GivenName], [p0].[Surname]
FROM [Operation].[Flight] AS [f]
LEFT JOIN [People].[Pilot] AS [p] ON [f].[Pilot_PersonID] = [p].[PersonID]
LEFT JOIN [People].[Employee] AS [e] ON [p].[PersonID] = [e].[PersonID]
LEFT JOIN [People].[Person] AS [p0] ON [e].[PersonID] = [p0].[PersonID]
WHERE [p0].[Surname] IS NULL
Folgender Code zeigt RightJoin() in einer Datenbank mit den Tabellen „Flight“ und „Pilot“:
///
/// Gibt zu den letzten drei angelegten Piloten alle Flüge aus
///
public void EFC10_RightJoin()
{
CUI.Demo(nameof(EFC10_RightJoin));
var ctx = new DA.WWWings.WwwingsV1EnContext();
CUI.H2("Alt: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via GroupJoin() und SelectMany()");
var ctx2 = new DA.WWWings.WwwingsV1EnContext();
#region --------------- ALT
var pilotenMitFlugAlt = ctx.Pilots
.OrderByDescending(x => x.PersonId)
.Take(3)
.GroupJoin(
ctx.Flights,
p => p.PersonId,
f => f.PilotPersonId,
(p, flights) => new { Pilot = p, Flights = flights.DefaultIfEmpty() }
)
.SelectMany(
pf => pf.Flights,
(pf, f) => new
{
PilotId = pf.Pilot.PersonId,
pf.Pilot.Employee.Person.GivenName,
pf.Pilot.Employee.Person.Surname,
Flight = f,
f.Departure,
f.Destination,
}
)
.OrderBy(x => x.PilotId)
.ToList();
foreach (var p in pilotenMitFlugAlt)
{
Console.WriteLine($"Pilot #{p.PilotId} {p.GivenName} {p.Surname} fliegt " + (p.Flight != null ? $"Flug #{p.Flight?.FlightNo} {p.Flight.Departure}->{p.Flight.Destination} am {p.Flight.FlightDate}" : "bisher keinen Flug"));
}
Console.WriteLine("Anzahl: " + pilotenMitFlugAlt.Count);
#endregion
#region --------------- NEU
CUI.H2("Neu: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via RightJoin()");
var pilotenMitFlugNeu = ctx.Flights
.RightJoin(
ctx.Pilots.OrderByDescending(x => x.PersonId).Take(3),
f => f.PilotPersonId,
p => p.PersonId,
(f, p) => new
{
PilotId = p.PersonId,
p.Employee.Person.GivenName,
p.Employee.Person.Surname,
Flight = f,
f.Departure,
f.Destination,
}).OrderBy(x => x.PilotId).ToList();
foreach (var p in pilotenMitFlugNeu)
{
Console.WriteLine($"Pilot #{p.PilotId} {p.GivenName} {p.Surname} fliegt " + (p.Flight != null ? $"Flug #{p.Flight?.FlightNo} {p.Flight.Departure}->{p.Flight.Destination} am {p.Flight.FlightDate}" : "bisher keinen Flug"));
}
Console.WriteLine("Anzahl: " + pilotenMitFlugNeu.Count);
#endregion
#region --------------- Kontrolle
CUI.H2("Nur zur Kontrolle: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via Navigation Property");
var pilotenMitFlug2 = ctx.Pilots.Include(p => p.Employee).ThenInclude(p => p.Person).Include(p => p.Flights).OrderByDescending(x => x.PersonId).Take(3).OrderBy(x => x.PersonId).ToList();
int count = 0;
foreach (Pilot p in pilotenMitFlug2)
{
if (p.Flights.Count == 0)
{
count++;
Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt bisher keinen Flug");
}
else
{
foreach (var f in p.Flights)
{
count++;
Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt Flug #{f.FlightNo} {f.Departure}->{f.Destination} am {f.FlightDate}");
}
}
}
Console.WriteLine("Anzahl: " + count);
#endregion
}
Aus dem LINQ-Befehl mit RightJoin() entsteht folgender SQL-Befehl:
SELECT [p0].[PersonID] AS [PilotId], [p1].[GivenName], [p1].[Surname], [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination], [f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID], [f].[Seats], [f].[Timestamp]
FROM [Operation].[Flight] AS [f]
RIGHT JOIN (
SELECT TOP(@p) [p].[PersonID]
FROM [People].[Pilot] AS [p]
ORDER BY [p].[PersonID] DESC
) AS [p0] ON [f].[Pilot_PersonID] = [p0].[PersonID]
INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID]
INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID]
ORDER BY [p0].[PersonID]
Es sei zudem explizit darauf hingewiesen, dass man die neuen Operatoren RightJoin() und LeftJoin() einschließlich der vorher schon vorhandenen Operatoren Join() und GroupJoin() nur für die Verbindung von Tabellen braucht, für die es im Objektmodell keine Navigationsbeziehung gibt.
So kann man statt des aufwendigen RightJoin() bei einer vorhandenen Navigationsbeziehung im Objektmodell dasselbe Ausgabeergebnis mit einem Include() erreichen. In diesem Fall erhält man allerdings keine flache Liste mit Daten aus Pilot und Flug, sondern eine Objekthierarchie, daher zwei verschachtelte foreach-Schleifen.
Folgender Code verwendet ein Include() über Navigationsbeziehung statt RightJoin():
CUI.H2("Gibt zu den letzten drei angelegten Piloten alle Flüge aus via Navigation Property");
var pilotenMitFlug2 = ctx.Pilots.Include(p => p.Employee).ThenInclude(p => p.Person).Include(p => p.Flights).OrderByDescending(x => x.PersonId).Take(3).OrderBy(x => x.PersonId).ToList();
foreach (Pilot p in pilotenMitFlug2)
{
if (p.Flights.Count == 0)
{
Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt bisher keinen Flug");
}
else
{
foreach (var f in p.Flights)
{
Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt Flug #{f.FlightNo} {f.Departure}->{f.Destination} am {f.FlightDate}");
}
}
}
Entity Framework Core führt dabei nur Inner Joins aus:
SELECT [p0].[PersonID], [p0].[FlightHours], [p0].[FlightSchool], [p0].[LicenseDate], [p0].[LicenseType], [e].[PersonID], [e].[EmployeeNo], [e].[HireDate], [e].[Supervisor_PersonId], [p1].[PersonID], [p1].[Birthday], [p1].[City], [p1].[Country], [p1].[EMail], [p1].[GivenName], [p1].[Memo], [p1].[Photo], [p1].[Surname], [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination], [f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID], [f].[Seats], [f].[Timestamp]
FROM (
SELECT TOP(@p) [p].[PersonID], [p].[FlightHours], [p].[FlightSchool], [p].[LicenseDate], [p].[LicenseType]
FROM [People].[Pilot] AS [p]
ORDER BY [p].[PersonID] DESC
) AS [p0]
INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID]
INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID]
LEFT JOIN [Operation].[Flight] AS [f] ON [p0].[PersonID] = [f].[Pilot_PersonID]
ORDER BY [p0].[PersonID], [e].[PersonID], [p1].[PersonID]
(rme)
Entwicklung & Code
OpenSpec: Software von KI mit Spezifikationen entwickeln
Das Tool OpenSpec für Spec-driven Development führt in Version 1.6 einen Update-Befehl ein und unterstützt Projekte mit dem Coding-Agenten Oh My Pi und dem KI-Editor TRAE.
Weiterlesen nach der Anzeige
Mit dem neuen Befehl /opsx:update können Entwicklerinnen und Entwickler eine vorhandene, mit OpenSpec erzeugte Spezifikation vor der Implementierung ändern, ohne einen komplett neuen Anlauf nehmen zu müssen.
Kommandos und Skills
Außerdem erzeugt OpenSpec nun Kommandos und Skills für den CLI-Agenten Oh My Pi und für die KI-Entwicklungsumgebung TRAE von ByteDance. Für Prompts hat das OpenSpec-Team zudem die Genehmigungsregeln vereinfacht, die Entwickler jetzt pauschal im Vorfeld erteilen können. Auch die Validierung von Anforderungen an ein neues Projekt arbeitet laut Release Notes nun konsistenter.
Das Kommandozeilen-Tool OpenSpec organisiert KI-Projekte mit Spec-driven Development. Das heißt, Entwickler teilen dem Tool ihre Idee mit (/opsx:explore), wobei sich auch explizit bestehender Code einbinden lässt. Das Tool arbeitet also auch brown field.
KI-Agenten führen
Mit /opsx:propose erzeugt OpenSpec dann die Spezifikation in verschiedenen Ordnern und Markdown-Dateien. Die Spezifikation dient als Grundlage für die eigentliche Entwicklung und führt insbesondere KI-Agenten in die richtige Richtung. Eine Ablagemöglichkeit der Specs in einem Repo ist noch beta und soll als zentrale Single Source of Truth für ein Team dienen.
Weiterlesen nach der Anzeige
Sobald die Specs fertig sind und kein /opsx:update erforderlich war, startet man die Umsetzung mit /opsx:apply. Dabei arbeitet OpenSpec mit über 25 Tools zusammen, wie Antigravity, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Junie, Kiro, Mistral Vibe, Qwen Code und neu eben Oh My Pi sowie TRAE. Als Modelle für OpenSpec selbst empfiehlt der Herausgeber Codex 5.5 oder Opus 4.7.
OpenSpec läuft auf Node ab Version 20.19 und ist Open Source unter MIT-Lizenz. KI-Contributions sind willkommen, solange sie geprüft und mit Modellbezeichnung im Pull Request erfolgen.
(who)
Entwicklung & Code
Wiederverwendbare Rakete: China fängt erstmals Stufe auf
Erstmals holte das Land die erste Stufe einer Trägerrakete kontrolliert zurück. Die Stufe der neuen Rakete „Langer Marsch 10B“ wurde auf einer Plattform auf See aufgefangen, wie die staatliche Nachrichtenagentur Xinhua berichtete.
Weiterlesen nach der Anzeige
Es war der erste Flug der neuen Rakete. Xinhua sprach von einem „bedeutenden Durchbruch“. Ein von der Agentur veröffentlichtes Video zeigt, wie die Raketenstufe mit laufendem Triebwerk auf die Plattform zufliegt und nahezu senkrecht in ein hohes, rechteckiges Gerüst sinkt.
Nach Angaben der Zeitung „China Daily“ wartete das Rückholschiff „Linghangzhe“ („Pfadfinder“) auf die Raketenstufe. Das Schiff verfolgte sie und passte seine Position laufend an. Spezielle Metallhaken klappten schließlich aus und griffen in die gespannten Seile der Fangvorrichtung. Die Rakete war vom Weltraumbahnhof Hainan gestartet und brachte einen Satelliten in die vorgesehene Umlaufbahn.
SpaceX ist schon deutlich weiter
China sei laut der staatlichen Zeitung damit nach den USA das zweite Land, das über eine zuverlässige Technik für wiederverwendbare Raketen verfüge. Zugleich sei weltweit erstmals die erste Stufe einer mit einer solchen Seil-Fangvorrichtung geborgen worden.
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.
Vor allem das US-Raumfahrtunternehmen SpaceX ist bei wiederverwendbaren Raketen deutlich weiter. China treibt die Entwicklung seit Jahren voran. Neben dem staatlichen Raumfahrtprogramm arbeiten auch mehrere private chinesische Unternehmen an der Technik, die es ermöglichen soll, Satelliten und andere Lasten günstiger ins All zu bringen.
Weiterlesen nach der Anzeige
Lesen Sie auch
(afl)
Entwicklung & Code
TypeScript 7.0: Performance-Sprung durch Go-Unterbau
Microsoft hat die finale Version von TypeScript 7.0 veröffentlicht. Im Gegensatz zu früheren TypeScript-Versionen basiert der Compiler nicht mehr auf JavaScript/Node.js, sondern ist nahezu eins zu eins in Go nachimplementiert und parallelisiert viele Arbeitsabläufe, statt sie wie bisher sequenziell abzuarbeiten. Beides führt laut Microsoft dazu, dass die Toolchain je nach Anwendungsfall zehnmal schneller kompiliert als bei TypeScript 6.0.
Weiterlesen nach der Anzeige
Gegenüber der im April veröffentlichten Beta-Version hat sich nichts Grundlegendes mehr geändert. Das finale Release konzentriert sich auf den produktionsreifen Abschluss, wie Microsoft im Blogpost zu TypeScript 7.0 schreibt: Umstellung auf das reguläre typescript-Paket, verbesserter --watch-Modus, vollständigerer Editor-Support, mehr Stabilität und klarere Migrationspfade für das TypeScript-Ökosystem.
Schneller kompiliert und geladen, weniger Speicherhunger
In seinem Blog nennt Microsoft Performance-Beispiele, die sich jeweils auf einen hardwareseitig nicht näher spezifizierten Computer beziehen.
Den Quellcode von VS-Code baut TypeScript 7.0 auf dem Testsystem um den Faktor 11,9 schneller als unter TypeScript 6.0, was die Kompilierungszeit von mehr als zwei Minuten auf knapp 11 Sekunden drückt. Bei Sentry, Bluesky und Playwright ergeben sich Beschleunigungsfaktoren von jeweils knapp unter 9 Sekunden, tldraw-Beispielprojekte kommen auf 7,7-faches Tempo.

VSCode-Beispielprojekte baut TypeScript 7.0 11,9-mal schneller als TypeScript 6.0.
(Bild: Microsoft)
Das Öffnen einer fehlerbehafteten Datei in VS Code auf dem gleichen Testsystem läuft ebenfalls schneller ab. Vergehen bei TypeScript 6.0 noch 17,5 Sekunden vom Öffnen einer fehlerhaften Datei bis zur Anzeige der Fehlermeldung, dauert das Gleiche bei TypeScript 7.0 unter 1,3 Sekunden. Neben dem Geschwindigkeitszuwachs ergibt sich laut Microsoft projektübergreifend auch ein geringerer Speicherbedarf. Während er bei Sentry um sechs Prozent sinkt, reduziert er sich bei VS Code um 18 Prozent und bei Bluesky um etwas mehr als ein Viertel.
Weiterlesen nach der Anzeige
TypeScript 6.0/7.0: Parallelbetrieb möglich
TypeScript 7.0 lässt sich via npm mit npm install -D typescript parallel zu TypeScript 6.0 installieren und ist mit dessen Type-Checking- und Kommandozeilenverhalten kompatibel. Der bekannte Befehl tsc startet dann den neuen TypeScript-7.0-Compiler. Zusätzlich gibt es das Kompatibilitätspaket @typescript/typescript6, das zusätzlich tsc6 sowie die bisherige TypeScript-6.0-API bereitstellt, damit bestehende Tools vorerst unverändert weiterlaufen können. Eine neue API soll erst mit TypeScript 7.1 kommen.
Lesen Sie auch
(mro)
-
Künstliche Intelligenzvor 3 Monaten
xTool P3 im Test: CO₂-Laser mit 80 Watt schneidet und graviert auch Acryl
-
UX/UI & Webdesignvor 1 WocheRegional & mit Gefühl: Identity für Klimafonds Baden-Württemberg › PAGE online
-
Künstliche Intelligenzvor 3 MonatenWeitere Entlassungswelle bei Disney: Bis zu 1000 Mitarbeiter betroffen
-
Apps & Mobile Entwicklungvor 3 MonatenMega-GPUs für Nvidia, AMD & Co: TSMC zeigt CoWoS-Package mit >11.600 mm² & 24 × HBM5E
-
Social Mediavor 2 MonatenMetas neuer Creative Setup Workflow: Was sich wirklich ändert – und warum das nicht nur eine UI-Frage ist!
-
Künstliche Intelligenzvor 3 MonatenApple‑Geräte mit Microsoft Intune verwalten – zweiteiliges Live-Webinar
-
UX/UI & Webdesignvor 2 MonatenHornbach – und die anderen Gewinner der ADC Annual Awards New York! › PAGE online
-
Apps & Mobile Entwicklungvor 3 MonatenMutter Palit dementiert: Gerüchteküche beerdigt fälschlicherweise Galax/KFA²
