Hinweis: Dieser Podcast und dieses Video wurden mithilfe von KI erstellt. Dabei wurden die Originalinhalte und technischen Erkenntnisse des Autors des Blogbeitrags adaptiert.
Hard Links im Dateisystem
Bei den Dateisystemklassen im Namensraum System.IO bietet die Klasse File nun die neuen Methoden File.CreateHardLink() und FileInfo.CreateAsHardLink() zum Anlegen von Hard Links an. Zuvor konnten Entwicklerinnen und Entwickler seit .NET 6.0 nur symbolische Links mit File.CreateSymbolicLink() anlegen, was jedoch immer Administratorrechte erforderte. Tabelle 1 und die Abbildungen 1 und 2 zeigen den Vergleich der beiden Dateisystem-Linkarten.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
| Merkmal | Symbolischer Link |
Hard Link |
|---|---|---|
|
Prinzip |
Verweis auf einen Pfad |
zweiter Dateieintrag für dieselbe Datei |
|
Bezug |
pfadbasiert |
Dateisystem-/Inode-basiert |
|
funktioniert über Laufwerke hinweg |
ja |
nein |
|
funktioniert für Verzeichnisse |
ja |
normalerweise nein (unter Windows nur Dateien) |
|
Ziel darf fehlen |
ja (Dangling Link) |
nein |
|
Löschen des Originals |
Link bleibt bestehen, aber defekt |
Datei bleibt erhalten |
|
Größe des Links |
kleine eigene Datei |
kein eigener Inhalt |
|
Erkennbarkeit im Explorer |
als Verknüpfung sichtbar | nicht sichtbar |
| API in .NET | File.CreateSymbolicLink() |
File.CreateHardLink() |
|
verfügbar in .NET seit |
.NET 6.0 | .NET 11.0 |
| benötigt Adminrechte oder Developer Mode | ja |
nein |
Tabelle 1: Symbolischer Link und Hard Link im Vergleich

Abb. 1: Ein Hard Link und ein symbolischer Link im Dateisystem
Abb. 2: Ausgabe von Listing 2
In den Listings 1-3 werden ein symbolischer Link und ein Hard Link angelegt.
Listing 1: Anlegen eines Hard Links und eines symbolischen Links
public void SymLink()
{
CUI.Demo(nameof(SymLink));
CUI.Print($"Administratorrechte: \e[93m{PrivilegeUtil.IsAdministrator()}\e[0m Developer Mode: \e[93m{PrivilegeUtil.IsDeveloperModeEnabled()}\e[0m");
// Zielverzeichnis und Datei
string zielVerzeichnis = @"T:\NET11Demo\Texte\Textbausteine";
string zielDatei = Path.Combine(zielVerzeichnis, "Textbausteine.txt");
// Symbolischer Link
string symLink = @"T:\NET11Demo\Textbausteine-SymLink.txt";
// Verzeichnis anlegen, falls nicht vorhanden
Directory.CreateDirectory(zielVerzeichnis);
// Textdatei erzeugen
File.WriteAllText(
zielDatei,
"Dies ist eine Beispieldatei.");
Console.WriteLine("Datei erzeugt:");
Console.WriteLine(zielDatei);
// Vorhandenen Link löschen
if (File.Exists(symLink))
{
File.Delete(symLink);
}
// Symbolischen Link erzeugen
try
{
var info = File.CreateSymbolicLink(symLink, zielDatei);
Console.WriteLine();
Console.WriteLine("Symbolischer Link erzeugt:");
Console.WriteLine($"{info.FullName} ({info.CreationTime})");
// Test
Console.WriteLine();
Console.WriteLine($"Datei {symLink} ist vom Typ: \e[93m{FileTypeUtil.GetFileType(symLink)}\e[0m");
Console.Write("Inhalt über den Link gelesen: ");
CUI.Print(File.ReadAllText(symLink), ConsoleColor.Yellow);
}
catch (Exception ex)
{
CUI.Error(ex);
}
}
/// <summary>
public void HardLink()
{
CUI.Demo(nameof(HardLink));
CUI.Print($"Administratorrechte: \e[93m{PrivilegeUtil.IsAdministrator()}\e[0m Developer Mode: \e[93m{PrivilegeUtil.IsDeveloperModeEnabled()}\e[0m");
// Zielverzeichnis und Datei
string zielVerzeichnis = @"T:\NET11Demo\Texte\Textbausteine";
string zielDatei = Path.Combine(zielVerzeichnis, "Textbausteine.txt");
// Hard Link
string hardLink = @"T:\NET11Demo\Textbausteine-HardLink.txt";
// Verzeichnis anlegen, falls nicht vorhanden
Directory.CreateDirectory(zielVerzeichnis);
// Textdatei erzeugen
File.WriteAllText(
zielDatei,
"Dies ist eine Beispieldatei.");
Console.WriteLine("Datei erzeugt:");
Console.WriteLine(zielDatei);
// Vorhandenen Link löschen
if (File.Exists(hardLink))
{
File.Delete(hardLink);
}
// Hard Link erzeugen
var info = File.CreateHardLink(hardLink, zielDatei);
Console.WriteLine();
Console.WriteLine("Hard Link erzeugt:");
Console.WriteLine($"{info.FullName} ({info.CreationTime})");
// Test
Console.WriteLine();
Console.WriteLine($"Datei {hardLink} ist vom Typ: \e[93m{FileTypeUtil.GetFileType(hardLink)}\e[0m");
Console.Write("Inhalt über den Link gelesen: ");
CUI.Print(File.ReadAllText(hardLink), ConsoleColor.Yellow);
}
Listing 2: Hilfsklasse PrivilegeUtil zum Ermitteln der Rechte
using System.Security.Principal;
using Microsoft.Win32;
public static class PrivilegeUtil
{
public static bool IsAdministrator()
{
return new WindowsPrincipal(
WindowsIdentity.GetCurrent())
.IsInRole(WindowsBuiltInRole.Administrator);
}
public static bool IsDeveloperModeEnabled()
{
const string keyPath =
@"SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
const string valueName =
"AllowDevelopmentWithoutDevLicense";
using RegistryKey? key =
Registry.LocalMachine.OpenSubKey(keyPath);
if (key == null)
{
return false;
}
object? value = key.GetValue(valueName);
return value is int intValue && intValue == 1;
}
}
Listing 3: Hilfsklasse FileTypeUtil zum Ermitteln, ob eine Datei eine echte Datei oder ein Link ist
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public static class FileTypeUtil
{
public static string GetFileType(string path)
{
if (!File.Exists(path))
{
return "Datei existiert nicht";
}
FileInfo fi = new(path);
// Symbolischer Link?
if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
return "Symbolic Link";
}
// Hard Link?
uint linkCount = GetHardLinkCount(path);
if (linkCount > 1)
{
return "Hard Link";
}
return "Echte Datei";
}
private static uint GetHardLinkCount(string path)
{
using FileStream fs = File.Open(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete);
if (!GetFileInformationByHandle(
fs.SafeFileHandle.DangerousGetHandle(),
out BY_HANDLE_FILE_INFORMATION info))
{
throw new IOException(
"GetFileInformationByHandle fehlgeschlagen.");
}
return info.NumberOfLinks;
}
[StructLayout(LayoutKind.Sequential)]
private struct BY_HANDLE_FILE_INFORMATION
{
public uint FileAttributes;
public FILETIME CreationTime;
public FILETIME LastAccessTime;
public FILETIME LastWriteTime;
public uint VolumeSerialNumber;
public uint FileSizeHigh;
public uint FileSizeLow;
public uint NumberOfLinks;
public uint FileIndexHigh;
public uint FileIndexLow;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(
IntPtr hFile,
out BY_HANDLE_FILE_INFORMATION lpFileInformation);
}
Vereinfachter Start von Prozessen
Die Klasse System.Diagnostics.Process zur Verwaltung von Betriebssystemprozessen existiert in der .NET-Basisklassenbibliothek seit Version 1.0 des klassischen .NET Frameworks aus dem Jahr 2002. Prozesse startet man seitdem, indem man eine neue Instanz der Klasse erzeugt. Seit .NET Framework 2.0 (Jahr 2005) gibt es alternativ die statische Methode Process.Start().
In .NET 11.0 ergänzt Microsoft nun vier weitere alternative statische Methoden zum Prozessstart:
-
Process.Run() und Process.RunAsync() sowie
-
Process.RunAndCaptureText() und Process.RunAndCaptureTextAsync()
Das letztgenannte Pärchen liefert ein ProcessTextOutput-Objekt, mit dem man direkt auf die Standardausgabe (ProcessTextOutput), die Standardfehlerausgabe (StandardError) und den Rückgabewert (ExitStatus.ExitCode) zugreifen kann mit deutlich weniger Programmcode, als das bei der alten Start()-Methode notwendig ist (Listing 4). Ein Abbruch des Kindprozesses ist über ein Cancellation-Token möglich. Anders als bei der Start()-Methode kehren alle neuen Methoden mit „Run“ im Namen erst zum Aufrufer zurück, wenn der Kindprozess beendet ist. Entwicklerinnen und Entwickler können dabei allerdings keine Ausgaben des Prozesses verarbeiten, während er läuft.
Listing 4: Einsatz der neuen Methode Process.RunAndCaptureTextAsync()
CancellationTokenSource cts = new CancellationTokenSource();
ProcessTextOutput result = await Process.RunAndCaptureTextAsync(
"robocopy.exe", [@"t:\Daten", @"t:\Daten_Backup", "/MIR", "/IS"], cts.Token);
CUI.Print("Neuer Prozess mit ID #" + result.ProcessId + " ist beendet!");
CUI.Line("StandardOutput");
CUI.Print(result.StandardOutput);
CUI.Line("StandardError");
CUI.PrintError(result.StandardError);
CUI.Line("ExitStatus");
CUI.Print("Canceled? " + result.ExitStatus.Canceled);
if (result.ExitStatus.HasValue && !result.ExitStatus.IsEmpty) PrintStatus(result.ExitStatus.ExitCode);
Eine weitere in .NET 11.0 hinzugefügte Methode zum Prozessstart ist Process.StartAndForget() zum Start eines Prozesses, ohne auf den erfolgreichen Start zu warten und ohne direkte Interaktionsmöglichkeiten mit dem neuen Prozess (Listing 5). Entwicklerinnen und Entwickler können lediglich über die zurückgelieferte Prozess-ID den neuen Prozess von außen überwachen, haben aber keinen Zugriff auf den Rückgabewert des Prozesses. Beim Versuch, ExitCode auszulesen, kassiert man den Laufzeitfehler „System.InvalidOperationException: ‘Process was not started by this object, so requested information cannot be determined.’“.
Listing 5: Einsatz der neuen Methode Process.StartAndForget()
int processId = Process.StartAndForget(
"robocopy.exe", [@"t:\Daten", @"t:\Daten_Backup", "/MIR", "/IS"]);
CUI.Print("Neuer Prozess mit ID #" + processId + " ist gestartet!");
var p = Process.GetProcessById(processId);
while(!p.HasExited)
{
CUI.BusyIndicator();
Thread.Sleep(500);
}
CUI.Line("Neuer Prozess mit ID #" + processId + " ist beendet!");
In der Klasse ProcessStartInfo, die bei Process.Start() zum Einsatz kommt, gibt es in .NET 11.0 zwei neue Boolean-Optionen:
-
ProcessStartInfo.StartDetached = true erzeugt einen unabhängigen Prozess mit eigener Konsole, der weiterlebt, auch wenn der startende Prozess beendet wird.
-
Mit ProcessStartInfo.KillOnParentExit = true erreicht man, dass der Kindprozess endet, wenn der startende Prozess endet. Wenn man beide Optionen in Kombination einsetzt, erhält man eine separate Konsole, die aber endet, wenn der startende Prozess endet.
Während ProcessStartInfo.StartDetached auf allen Plattformen läuft, meldet ProcessStartInfo.KillOnParentExit aktuell in Preview 4, dass es nur auf Windows funktioniert. In einem Blogeintrag [1] findet man schon den Hinweis darauf, dass Implementierungen für Android und Linux in Arbeit sind.
Für mit Process.Start() gestartete Prozesse gibt es auch noch neu die Methoden ReadAllText() und ReadAllTextAsync(), mit denen man von einem beendeten Prozess gleichzeitig die komplette Standardausgabe und die Fehlerausgabe als ein Tupel aus zwei Zeichenketten bekommen kann:
process.WaitForExit();
(string output, string error) = process.ReadAllText();
Im Gegensatz zum bisherigen Ansatz
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
besteht bei den neuen Methoden keine Gefahr eines Deadlocks.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Author
🔍 Frequently Asked Questions (FAQ)
1. Was ist neu in .NET 11 Preview 1 bis 4?
.NET 11 Preview 1 bis 4 bringt mehrere Neuerungen für die .NET-Basisklassenbibliothek. Der Artikel behandelt insbesondere neue APIs für Hard Links in System.IO sowie neue Möglichkeiten zum Starten und Verwalten von Prozessen mit System.Diagnostics.Process.
2. Wie lassen sich Hard Links in .NET 11 erstellen?
.NET 11 führt mit File.CreateHardLink() und FileInfo.CreateAsHardLink() neue APIs zum Erstellen von Hard Links ein. Im Gegensatz zu symbolischen Links benötigen Hard Links laut Artikel weder Administratorrechte noch den Windows Developer Mode.
3. Was ist der Unterschied zwischen einem Hard Link und einem symbolischen Link in .NET?
Ein symbolischer Link verweist auf einen Pfad, während ein Hard Link einen weiteren Dateisystemeintrag für dieselbe Datei darstellt. Hard Links funktionieren nicht über Laufwerksgrenzen hinweg; außerdem bleibt die Datei erhalten, wenn einer ihrer Hard Links gelöscht wird.
4. Welche neuen Methoden zum Starten von Prozessen gibt es in .NET 11?
.NET 11 ergänzt Process.Run(), Process.RunAsync(), Process.RunAndCaptureText() und Process.RunAndCaptureTextAsync(). Die RunAndCaptureText-Varianten liefern direkten Zugriff auf Standardausgabe, Standardfehlerausgabe und Exit-Status und benötigen dafür weniger Programmcode als der bisherige Ansatz mit Process.Start().
5. Was macht Process.StartAndForget() in .NET 11?
Process.StartAndForget() startet einen Prozess, ohne auf dessen erfolgreichen Start zu warten und ohne eine direkte Interaktionsmöglichkeit mit dem gestarteten Prozess bereitzustellen. Zurückgegeben wird lediglich die Prozess-ID, über die sich der Prozess von außen überwachen lässt; auf seinen Exit-Code kann auf diesem Weg nicht direkt zugegriffen werden.
6. Was bewirken StartDetached und KillOnParentExit in .NET 11?
Mit ProcessStartInfo.StartDetached = true lässt sich ein unabhängiger Prozess mit eigener Konsole starten, der weiterläuft, wenn der startende Prozess beendet wird. ProcessStartInfo.KillOnParentExit = true bewirkt dagegen, dass der Kindprozess zusammen mit dem startenden Prozess beendet wird; laut Artikel funktioniert diese Option in Preview 4 derzeit nur unter Windows.





