ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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

DirectoryInfo class

In this article we have worked with directories in C# using the DirectoryInfo class.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.