📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

How do you build and handle file paths so that a .NET application works on both Windows and Linux? Explain absolute vs relative paths, separators, Path.Combine vs Path.Join, and case sensitivity.

Cross-platform path handling in .NET: separators, absolute vs relative paths, the current directory, Path.Combine vs Path.Join, case sensitivity and long paths.

Intermediate Theory Intermediate cross-platform file-system linux path-combine paths windows

🎯 Key points

  • Build paths with Path.Join or Path.Combine, and use Path.DirectorySeparatorChar (or '/') when you must format a path by hand; never hard-code backslashes.
  • Windows accepts both \ and /, while Linux and macOS use only /, and on Linux a backslash is a legal file name character.
  • Linux file systems are case-sensitive; Windows (NTFS) and default macOS (APFS) are case-insensitive, so Config.json versus config.json only fails after deployment.
  • Relative paths resolve against Environment.CurrentDirectory, which differs between dotnet run, Windows Services, containers (WORKDIR) and test runners.
  • Use AppContext.BaseDirectory for files shipped with the app, IHostEnvironment.ContentRootPath in ASP.NET Core, and configuration for data roots.
  • Path.Combine discards earlier segments when a later one is rooted; Path.Join simply concatenates with one separator. Neither is a security check.
  • Path.GetFullPath normalises . and .. and makes a path absolute, but does not touch the disk or resolve symbolic links; compare paths after normalising, using OrdinalIgnoreCase on Windows and Ordinal on Linux.
  • Run unit and integration tests on Linux in CI (or in the SDK container locally) so separator, case and long-path bugs are caught before release.

⏱ 30-second answer

  • Separators: Windows uses \ (and accepts /), Linux and macOS use only /. Never hard-code \; use Path.Combine/Path.Join or Path.DirectorySeparatorChar.
  • Absolute vs relative: C:\data\a.txt or /data/a.txt are absolute; data/a.txt is resolved against the current working directory, which differs between dotnet run, a service and a container. Base paths on AppContext.BaseDirectory or configuration.
  • Path.Combine drops earlier parts if a later part is rooted (Combine("/app", "/etc/passwd") gives /etc/passwd). Path.Join just concatenates with one separator.
  • Case: Linux file systems are case-sensitive, Windows and default macOS are not. Config.json vs config.json works on your laptop and fails in the Linux container.

πŸ—£ Full answer (senior)

The rules that bite in production

Topic Windows Linux macOS (default)
Separator \ (also accepts /) / only / only
Root C:\, \\server\share / /
Case sensitive No (NTFS can enable per folder) Yes No
Invalid name chars < > : " / \ | ? * and control chars / and NUL / and NUL (: awkward in Finder)
Reserved names CON, PRN, AUX, NUL, COM1… none none

Relative paths and the working directory

File.ReadAllText("appsettings.json") resolves against Environment.CurrentDirectory:

  • dotnet run from the project folder β†’ the project folder.
  • A Windows Service β†’ often C:\Windows\System32.
  • A Docker container β†’ the WORKDIR from the Dockerfile.
  • A test runner β†’ the test output folder.

Use AppContext.BaseDirectory for files deployed with the app, IHostEnvironment.ContentRootPath in ASP.NET Core, and configuration for data folders.

Combine vs Join

Path.Combine("/app/data", "reports", "a.csv");   // /app/data/reports/a.csv
Path.Combine("/app/data", "/etc/passwd");        // /etc/passwd   ← rooted part wins
Path.Join("/app/data", "/etc/passwd");           // /app/data//etc/passwd
Path.GetFullPath("reports/../a.csv", "/app/data"); // /app/data/a.csv

Path.Combine is fine for trusted segments. Neither method is a security check; for user input see path traversal protection.

Production example

A team developed on Windows and loaded templates with File.ReadAllText($"Templates\\{name}.html"). In the Linux container it failed: the backslash became part of the file name, and the folder on disk was templates in lowercase. The fix was Path.Join(AppContext.BaseDirectory, "Templates", $"{name}.html"), renaming the folder consistently, and running the test suite in a Linux container in CI so case and separator issues show up before release.

πŸ” What the interviewer is checking

Practical cross-platform awareness. Many teams now develop on Windows/macOS and deploy to Linux containers.

Green flags

  • Knows separators and case sensitivity differ.
  • Explains the current directory problem and uses AppContext.BaseDirectory / ContentRootPath.
  • Knows the Path.Combine rooted-argument behaviour and Path.Join.
  • Mentions running tests on Linux.

Red flags

  • String concatenation with backslashes.
  • Thinks Path.Combine prevents directory traversal.

πŸ“š Learn in depth

1. Anatomy of a path

Windows:   C:\Users\sam\app\data\report.csv
           β”‚   └──── directories β”€β”€β”€β”€β”˜β”” file name β”˜
           β”” drive root           extension β”€β”€β”˜

UNC:       \\fileserver\share\data\report.csv
Linux:     /home/sam/app/data/report.csv
           β”” root

2. Normalisation

Path.GetFullPath resolves . and .., fixes separators on Windows and makes the path absolute (against the current directory or a base you pass).

"/app/data/./reports/../a.csv"  ──GetFullPath──►  "/app/data/a.csv"

It does not touch the disk, so it does not resolve symbolic links and does not check existence.

3. Useful Path members

Member Example result
Path.GetFileName("/a/b/c.txt") c.txt
Path.GetFileNameWithoutExtension c
Path.GetExtension .txt
Path.GetDirectoryName /a/b
Path.IsPathRooted("/x") true
Path.IsPathFullyQualified("C:x") false on Windows (drive-relative)
Path.GetRelativePath("/a", "/a/b/c") b/c
Path.GetTempPath() / Path.GetRandomFileName() temp folder / random safe name
Path.GetInvalidFileNameChars() platform-specific list

4. Case sensitivity traps

Dev laptop (Windows/macOS):   Config.json == config.json   βœ”
Linux container:              Config.json != config.json   ✘ FileNotFoundException

The same applies to <Content Include="..."> items, static files served by ASP.NET Core and import paths in front-end bundles. Git on Windows/macOS may also not notice a rename that only changes case; use git mv Config.json config.json.

5. Long paths

Windows historically limited paths to 260 characters (MAX_PATH). Modern .NET handles longer paths on Windows, but other tools in your pipeline (old installers, some zip tools, Explorer in some cases) may not. Deep node_modules or nested build output are the usual triggers. Linux allows 4,096 bytes for a path and 255 bytes per name.

6. Try it yourself

  1. In a console app, print Environment.CurrentDirectory and AppContext.BaseDirectory, then run it with dotnet run and with dotnet bin/Debug/net10.0/App.dll from another folder.
  2. Print Path.Combine("/app", "/etc/hosts") and Path.Join("/app", "/etc/hosts").
  3. Run docker run --rm -v $PWD:/src -w /src mcr.microsoft.com/dotnet/sdk:10.0 dotnet test to catch case-sensitivity bugs locally.

πŸ’» Code

// Files shipped with the app: resolve from the app's folder, not the working directory
string templatePath = Path.Join(AppContext.BaseDirectory, "Templates", "welcome.html");
string html = await File.ReadAllTextAsync(templatePath, ct);

// Data folders: from configuration, normalised once at startup
var dataRoot = Path.GetFullPath(builder.Configuration["Storage:Root"] ?? "/var/lib/myapp");
Directory.CreateDirectory(dataRoot);

// Building a safe file name from user-provided text
static string ToSafeFileName(string input)
{
    var invalid = Path.GetInvalidFileNameChars();
    var chars = input.Select(c => invalid.Contains(c) ? '_' : c).ToArray();
    return new string(chars).Trim();
}
// Path.Combine vs Path.Join
Console.WriteLine(Path.Combine("/app/data", "reports", "2026.csv")); // /app/data/reports/2026.csv
Console.WriteLine(Path.Combine("/app/data", "/etc/passwd"));         // /etc/passwd
Console.WriteLine(Path.Join("/app/data", "/etc/passwd"));            // /app/data//etc/passwd
Console.WriteLine(Path.GetRelativePath("/app", "/app/data/a.csv"));  // data/a.csv

⚠ Common mistakes

Mistake: Hard-coding \\ in paths. Why it is wrong: On Linux the backslash is a valid file name character, so "Templates\\a.html" looks for one file literally named Templates\a.html. Instead: Use Path.Join or Path.Combine, or Path.DirectorySeparatorChar when formatting by hand.

Mistake: Relying on the current working directory. Why it is wrong: It changes between dotnet run, services, containers and test runners, so the same relative path points at different places. Instead: Anchor paths on AppContext.BaseDirectory, ContentRootPath or a configured absolute root.

Mistake: Assuming Path.Combine keeps you inside the base folder. Why it is wrong: A rooted later argument replaces everything before it, and .. segments can climb out. Instead: Normalise with Path.GetFullPath and check the result still starts with the root (plus a separator), or map IDs to stored paths instead of accepting user paths.

Mistake: Testing only on the developer's OS. Why it is wrong: Case sensitivity and separator bugs never appear on Windows or macOS, so they ship and fail in the Linux container. Instead: Add a Linux job to the CI pipeline (and a Windows one if you ship there) and run file-path tests on both.

Mistake: Comparing paths with ==. Why it is wrong: C:\Data\a.txt and c:/data/./a.txt are the same file on Windows but different strings. Instead: Normalise with Path.GetFullPath and compare with the platform's case rules.

πŸ› Senior insight: trade-offs

Most path bugs are environment bugs: code that works on a Windows laptop and breaks in a Linux container. I prevent them structurally rather than by being careful each time.

Practice What it prevents Cost
One place that defines roots (configuration or AppContext.BaseDirectory) Working-directory surprises Tiny
Only Path APIs to build paths Separator bugs None
Lowercase, hyphenated file names Case mismatches between dev and prod A naming convention
Linux and Windows CI jobs Platform-only failures Extra CI minutes
Startup check that required files exist Silent failures on first request A few lines of code
Normalise plus root-prefix check for user input Path traversal A small helper

Security and stability: Path.Combine and Path.GetFullPath are string operations, not security boundaries. For anything built from user input I normalise, check the result is still under the intended root, and prefer mapping opaque IDs to stored paths. On case-insensitive systems I also remember that two different-looking names can be the same file, which matters for allow-lists and caches keyed by path.

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate says Path.Combine keeps paths inside a folder Why: Tests whether they spot path traversal risk
    How to answer

    It is a path traversal vulnerability. file=../../../etc/passwd resolves outside the reports folder, and file=/etc/passwd is worse: Path.Combine discards the base path when the second argument is rooted. The endpoint also has no authorisation and leaks whether files exist.

    Fix: do not let users name files. Map an ID to a stored path (database), or if names are required, take only Path.GetFileName(file), build the path, Path.GetFullPath it and check it starts with the root plus a separator, using the platform's case rules. Serve through PhysicalFileProvider or Results.File with the checked path, and add authorisation.

  2. When: Candidate mentions running in Linux containers Why: Moves to permission issues in containers
    How to answer
    • Linux: each file has an owner (UID), a group (GID) and rwx bits for owner, group and others (for example 0644, 0755). On a directory, x means "can enter" and w means "can create, delete or rename entries". Root bypasses most checks.
    • Windows: each file has an ACL (a list of allow and deny entries for users and groups), usually inherited from the parent folder.

    The container error usually means the app runs as a non-root user (the official .NET 10 images define user app, UID 1654, via $APP_UID) while the mounted folder is owned by root with 0755. Fix it by giving that UID or a shared group write access (chown, Kubernetes fsGroup, or volume mount options), not by running the container as root or using chmod 777.

πŸ›  Real-world problems: how you handle them

Templates missing after moving from Windows servers to Linux containers
Situation
[Email service] on .NET [10] moved from [Windows VMs] to [Linux containers on AKS]. Templates were loaded from a relative path with backslashes.
Signals
`FileNotFoundException` for every email after the first Linux deployment; [all] welcome emails failed for [40 minutes].
Investigation
Logged the resolved path and saw `Templates\welcome.html` treated as one file name. Listed the container folder and found `templates/` in lowercase. Also noticed the working directory was `/app`, not the output folder.
Resolution
Rolled back, then changed to `Path.Join(AppContext.BaseDirectory, "templates", ...)`, renamed files to lowercase, and redeployed.
Prevention
Added a Linux test job in CI, a startup check that all templates exist, and a naming convention (lowercase, hyphens) in the contributing guide.

How to tell it (STAR)

**S:** We moved our [email service] from Windows VMs to Linux containers, and every email started failing. **T:** I had to restore emails and stop it happening again. **A:** I logged the resolved path and saw the backslash had become part of the file name, and the folder case did not match. I rolled back, switched to `Path.Join` with `AppContext.BaseDirectory`, normalised file names, and added a Linux test job plus a startup check. **R:** Emails were back within [an hour], and since then we have had [no] platform-specific path bugs reach production.

↩ Usually asked after

More in File System

Last updated 23 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me