C# DirectoryInfo
last modified November 6, 2026
In this article we show how to work with directories in C# using the
DirectoryInfo class.
A DirectoryInfo object represents one directory. Its properties
describe the directory, and its methods create, move, delete, and enumerate
it. The static Directory class
provides the same operations for paths given as strings. DirectoryInfo
is convenient when we perform several operations on the same directory, and
its enumeration methods return FileInfo and
DirectoryInfo objects instead of plain paths.
DirectoryInfo and FileInfo derive from the abstract
FileSystemInfo class, which defines the common members such as
Name, FullName, Exists,
Attributes, and the timestamps. The examples create a
demo directory in the working directory and delete it at the end;
the outputs come from Linux.
Creating an instance and cached state
Creating a DirectoryInfo object does not touch the file system,
and the directory need not exist. The state of the directory is read when a
property is first accessed and then cached.
var di = new DirectoryInfo(Path.Combine("demo", "reports"));
Console.WriteLine($"name: {di.Name}, exists: {di.Exists}");
di.Create();
Console.WriteLine($"after Create: {di.Exists}");
Directory.Delete(di.FullName);
Console.WriteLine($"after external delete: {di.Exists}");
di.Refresh();
Console.WriteLine($"after Refresh: {di.Exists}");
Directory.Delete("demo");
We create a directory with the object, delete it with the static
Directory class, and observe the Exists
property.
di.Create();
Console.WriteLine($"after Create: {di.Exists}");
The Create method creates the directory, including missing parent
directories. Operations performed through the object update its cached
state.
Directory.Delete(di.FullName);
Console.WriteLine($"after external delete: {di.Exists}");
Changes made by other code are not noticed. The object still reports that the directory exists.
di.Refresh();
The Refresh method reloads the state from the file system. Call
it before reading properties of an object that has lived for some time.
$ dotnet run name: reports, exists: False after Create: True after external delete: True after Refresh: False
Directory properties
The properties of DirectoryInfo give the name, location,
attributes, and timestamps of the directory.
var di = new DirectoryInfo(Path.Combine("demo", "projects", "app"));
di.Create();
Console.WriteLine($"Name: {di.Name}");
Console.WriteLine($"Parent: {di.Parent?.Name}");
Console.WriteLine($"Root: {di.Root}");
Console.WriteLine($"FullName ends with: {Path.GetRelativePath("demo", di.FullName)}");
Console.WriteLine($"Attributes: {di.Attributes}");
Console.WriteLine($"Created today: {di.CreationTime.Date == DateTime.Today}");
Console.WriteLine($"Last write UTC kind: {di.LastWriteTimeUtc.Kind}");
var hidden = new DirectoryInfo(Path.Combine("demo", ".cache"));
hidden.Create();
Console.WriteLine($"{hidden.Name}: {hidden.Attributes}");
Directory.Delete("demo", recursive: true);
We create a nested directory and print its properties.
Console.WriteLine($"Parent: {di.Parent?.Name}");
Console.WriteLine($"Root: {di.Root}");
Parent and Root return DirectoryInfo
objects. Parent is null for a root directory.
Console.WriteLine($"FullName ends with: {Path.GetRelativePath("demo", di.FullName)}");
FullName is the absolute path, resolved against the working
directory. We print only its end.
Console.WriteLine($"Last write UTC kind: {di.LastWriteTimeUtc.Kind}");
Each timestamp has a local and a UTC variant. Use the UTC variants when comparing times across time zones or daylight saving changes.
Console.WriteLine($"{hidden.Name}: {hidden.Attributes}");
On Unix systems, a name starting with a dot makes the directory hidden, and
.NET reports the Hidden attribute.
$ dotnet run Name: app Parent: projects Root: / FullName ends with: projects/app Attributes: Directory Created today: True Last write UTC kind: Utc .cache: Hidden, Directory
Listing files and directories
GetFiles returns an array of FileInfo objects and
GetDirectories an array of DirectoryInfo objects.
We can access their properties directly, without building paths.
CreateSample("demo");
var di = new DirectoryInfo("demo");
Console.WriteLine("Files:");
foreach (FileInfo file in di.GetFiles().OrderBy(f => f.Name))
{
Console.WriteLine($" {file.Name,-12} {file.Length,6} bytes {file.Extension}");
}
Console.WriteLine("Directories:");
foreach (DirectoryInfo dir in di.GetDirectories().OrderBy(d => d.Name))
{
Console.WriteLine($" {dir.Name}/ ({dir.GetFiles().Length} files)");
}
Directory.Delete("demo", recursive: true);
static void CreateSample(string root)
{
Directory.CreateDirectory(Path.Combine(root, "docs"));
Directory.CreateDirectory(Path.Combine(root, "images"));
File.WriteAllText(Path.Combine(root, "readme.txt"), new string('r', 120));
File.WriteAllText(Path.Combine(root, "data.csv"), new string('d', 4500));
File.WriteAllText(Path.Combine(root, "docs", "guide.md"), new string('g', 800));
File.WriteAllBytes(Path.Combine(root, "images", "logo.png"), new byte[2048]);
File.WriteAllBytes(Path.Combine(root, "images", "photo.jpg"), new byte[52000]);
}
We list the files of a directory with their sizes and extensions, and the subdirectories with the number of files they contain.
foreach (FileInfo file in di.GetFiles().OrderBy(f => f.Name))
The order of the entries is not guaranteed, so we sort them by name. The
Length property of FileInfo is the file size in
bytes.
Console.WriteLine($" {dir.Name}/ ({dir.GetFiles().Length} files)");
Each subdirectory is again a DirectoryInfo object, on which we
call GetFiles.
$ dotnet run Files: data.csv 4500 bytes .csv readme.txt 120 bytes .txt Directories: docs/ (1 files) images/ (2 files)
Creating, moving, and deleting
CreateSubdirectory creates directories under the current one,
MoveTo moves or renames the directory, and Delete
removes it.
var root = new DirectoryInfo("demo");
root.Create();
DirectoryInfo src = root.CreateSubdirectory(Path.Combine("src", "lib"));
Console.WriteLine($"created {Path.GetRelativePath(".", src.FullName)}");
DirectoryInfo tmp = root.CreateSubdirectory("tmp");
File.WriteAllText(Path.Combine(tmp.FullName, "x.tmp"), "temp");
DirectoryInfo old = root.CreateSubdirectory("old-name");
old.MoveTo(Path.Combine(root.FullName, "new-name"));
Console.WriteLine($"moved, Name is now: {old.Name}");
tmp.Delete(recursive: true);
foreach (var d in root.EnumerateDirectories("*", SearchOption.AllDirectories).OrderBy(d => d.FullName))
{
Console.WriteLine(Path.GetRelativePath(".", d.FullName));
}
root.Delete(recursive: true);
Console.WriteLine($"root exists: {root.Exists}");
We build a small tree, rename one directory, delete another, and list the result.
DirectoryInfo src = root.CreateSubdirectory(Path.Combine("src", "lib"));
The path is relative to the directory represented by the object, and it may contain several levels.
old.MoveTo(Path.Combine(root.FullName, "new-name"));
Console.WriteLine($"moved, Name is now: {old.Name}");
After MoveTo, the object represents the directory at its new
location. The destination must not exist and must be on the same volume.
tmp.Delete(recursive: true);
Delete(true) removes the directory with its contents;
Delete() works only for an empty directory.
$ dotnet run created demo/src/lib moved, Name is now: new-name demo/new-name demo/src demo/src/lib root exists: False
Directory size and statistics
Combined with LINQ, the enumeration methods make it easy to compute statistics about a directory tree.
CreateSample("demo");
var di = new DirectoryInfo("demo");
long total = di.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length);
Console.WriteLine($"total size: {total:N0} bytes");
Console.WriteLine("largest files:");
foreach (var f in di.EnumerateFiles("*", SearchOption.AllDirectories)
.OrderByDescending(f => f.Length).Take(3))
{
Console.WriteLine($" {f.Name,-10} {f.Length,7:N0}");
}
Console.WriteLine("size by extension:");
foreach (var g in di.EnumerateFiles("*", SearchOption.AllDirectories)
.GroupBy(f => f.Extension)
.OrderBy(g => g.Key))
{
Console.WriteLine($" {g.Key,-5} {g.Count()} files {g.Sum(f => f.Length),7:N0}");
}
foreach (FileSystemInfo e in di.EnumerateFileSystemInfos().OrderBy(e => e.Name))
{
string kind = e is DirectoryInfo ? "dir " : "file";
Console.WriteLine($"{kind} {e.Name}");
}
Directory.Delete("demo", recursive: true);
static void CreateSample(string root)
{
Directory.CreateDirectory(Path.Combine(root, "docs"));
Directory.CreateDirectory(Path.Combine(root, "images"));
File.WriteAllText(Path.Combine(root, "readme.txt"), new string('r', 120));
File.WriteAllText(Path.Combine(root, "data.csv"), new string('d', 4500));
File.WriteAllText(Path.Combine(root, "docs", "guide.txt"), new string('g', 800));
File.WriteAllBytes(Path.Combine(root, "images", "logo.png"), new byte[2048]);
File.WriteAllBytes(Path.Combine(root, "images", "photo.png"), new byte[52000]);
}
We compute the total size of a tree, find the largest files, sum the sizes per extension, and list the top-level entries.
long total = di.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length);
There is no property for the size of a directory. We sum the sizes of all
files in the tree. EnumerateFiles yields the files lazily,
without building an array first.
.GroupBy(f => f.Extension)
The Extension property includes the leading dot.
foreach (FileSystemInfo e in di.EnumerateFileSystemInfos().OrderBy(e => e.Name))
{
string kind = e is DirectoryInfo ? "dir " : "file";
EnumerateFileSystemInfos returns files and directories as
FileSystemInfo objects. A type pattern tells them apart.
$ dotnet run total size: 59,468 bytes largest files: photo.png 52,000 data.csv 4,500 logo.png 2,048 size by extension: .csv 1 files 4,500 .png 2 files 54,048 .txt 2 files 920 file data.csv dir docs dir images file readme.txt
Unix permissions
Since .NET 7, the UnixFileMode property reads and sets the Unix
permission bits of a directory. It is not supported on Windows, where
permissions are managed with access control lists through the
GetAccessControl and SetAccessControl extension
methods.
if (OperatingSystem.IsWindows())
{
Console.WriteLine("Unix file modes are not supported on Windows");
return;
}
var di = new DirectoryInfo("demo");
di.Create();
Console.WriteLine($"default: {di.UnixFileMode}");
di.UnixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
di.Refresh();
Console.WriteLine($"private: {di.UnixFileMode}");
DirectoryInfo shared = Directory.CreateDirectory(Path.Combine("demo", "shared"),
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute);
Console.WriteLine($"shared: {shared.UnixFileMode}");
di.Delete(recursive: true);
We print the default mode of a new directory, make it private, and create a subdirectory with explicit permissions.
if (OperatingSystem.IsWindows())
The check avoids a PlatformNotSupportedException on Windows and
also satisfies the platform compatibility analyzer.
di.UnixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
The mode 700 lets only the owner list, create, and enter the directory. On a directory, the execute bit allows entering it.
DirectoryInfo shared = Directory.CreateDirectory(Path.Combine("demo", "shared"),
Directory.CreateDirectory has an overload that sets the mode at
creation. The default mode depends on the user's umask; here it was 775.
$ dotnet run default: OtherExecute, OtherRead, GroupExecute, GroupWrite, GroupRead, UserExecute, UserWrite, UserRead private: UserExecute, UserWrite, UserRead shared: GroupExecute, GroupRead, UserExecute, UserWrite, UserRead
Symbolic links
Since .NET 6, DirectoryInfo can create and inspect symbolic links
to directories.
var target = Directory.CreateDirectory(Path.Combine("demo", "releases", "v2.1"));
File.WriteAllText(Path.Combine(target.FullName, "app.dll"), "binary");
var link = new DirectoryInfo(Path.Combine("demo", "current"));
link.CreateAsSymbolicLink(Path.Combine("releases", "v2.1"));
link.Refresh();
Console.WriteLine($"link target: {link.LinkTarget}");
Console.WriteLine($"is reparse point: {link.Attributes.HasFlag(FileAttributes.ReparsePoint)}");
Console.WriteLine($"files via link: {string.Join(", ", link.GetFiles().Select(f => f.Name))}");
FileSystemInfo? resolved = link.ResolveLinkTarget(returnFinalTarget: true);
Console.WriteLine($"resolved: {resolved?.Name}");
link.Delete();
Console.WriteLine($"target still exists: {target.Exists}");
Directory.Delete("demo", recursive: true);
We create a current link pointing to a release directory, as
deployment tools often do.
link.CreateAsSymbolicLink(Path.Combine("releases", "v2.1"));
CreateAsSymbolicLink turns the path of the object into a link to
the given target. A relative target is resolved against the directory that
contains the link. On Windows, creating symbolic links requires administrator
rights or Developer Mode.
Console.WriteLine($"is reparse point: {link.Attributes.HasFlag(FileAttributes.ReparsePoint)}");
Links have the ReparsePoint attribute. LinkTarget
returns the target path as it was stored, and
ResolveLinkTarget returns the target as a
FileSystemInfo.
link.Delete();
Deleting a link removes only the link, not the target directory.
$ dotnet run link target: releases/v2.1 is reparse point: True files via link: app.dll resolved: v2.1 target still exists: True
Source
In this article we have worked with directories in C# using the
DirectoryInfo class.
Author
List all C# tutorials.