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

📚 Interview question

How do source files, content files and embedded resources work in a .NET build, and how do you read an embedded resource at run time?

Build items for files: Compile, None, Content and EmbeddedResource, CopyToOutputDirectory and CopyToPublishDirectory, embedded resource naming and LogicalName, reading with GetManifestResourceStream, .resx localisation, and when to embed versus copy.

Intermediate Theory Intermediate build-system content-files embedded-resources msbuild

🎯 Key points

  • Compile items are C# sources (globbed **/*.cs); None items are other files that do nothing unless told to
  • CopyToOutputDirectory (PreserveNewest or Always) copies a file next to the app; CopyToPublishDirectory controls publish
  • Content is used by web projects for wwwroot and similar files that are published
  • EmbeddedResource compiles the file into the assembly; read it with Assembly.GetManifestResourceStream
  • Default embedded resource names are RootNamespace plus the folder path with dots; LogicalName sets an explicit name
  • .resx files are EmbeddedResource items used for localisation through ResourceManager and IStringLocalizer
  • Embed small read-only assets that must travel with the code; copy files that operators may change or that are large

⏱ 30-second answer

SDK-style projects classify files into item types. Compile is C# source, picked up automatically. None is everything else, and it does nothing unless you set metadata such as CopyToOutputDirectory="PreserveNewest" to copy the file next to the app. Web projects use Content for files that get published, like wwwroot. EmbeddedResource compiles the file into the assembly. You read it with Assembly.GetManifestResourceStream(name). The default name is the root namespace plus the folder path joined with dots, and LogicalName lets you choose a stable name. .resx files are embedded resources used for localisation. I embed small, read-only assets that must travel with the code, such as SQL scripts, templates or schemas. Files that operators might change, or large ones, I copy to the output instead.

🗣 Full answer (senior)

How I would answer this in an interview

"SDK-style projects glob files by default and classify each one as an item type. Compile is C# source, picked up from **/*.cs. None is everything else, and it does nothing unless you add metadata: CopyToOutputDirectory set to PreserveNewest copies the file next to the app, and CopyToPublishDirectory controls publish. Because the file is already globbed, I use Update rather than Include to set that metadata. Web projects use Content for things like wwwroot that get published.

EmbeddedResource compiles the file into the assembly, and I read it with Assembly.GetManifestResourceStream. The default name is the root namespace plus the folder path joined with dots, and the mistake I see is relying on that: rename a folder and the lookup silently returns null. I set an explicit LogicalName instead, and when a stream is null I print GetManifestResourceNames() to see the real names. .resx files are embedded resources too, used for localisation through ResourceManager or IStringLocalizer.

Embed versus copy is really a deployment decision. I embed small, read-only assets that must travel with the code, like SQL scripts, templates or schemas, so they can't go missing. Large files, or ones operators might change, I copy and locate with AppContext.BaseDirectory, which also works for single-file apps. Either way I never put secrets in them, I read once and cache, and I add a test that loads every resource the code depends on."


Item types for files

Item Default in SDK projects Result
Compile **/*.cs Compiled into the assembly
EmbeddedResource **/*.resx Stored inside the assembly as a manifest resource
None Other files in the folder Ignored by the build unless metadata says otherwise
Content Web SDK: wwwroot/**, appsettings*.json and similar Copied to output and publish

Copying files

<ItemGroup>
  <None Update="data/postcodes.csv" CopyToOutputDirectory="PreserveNewest" />
  <None Update="tools/**" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

Update changes metadata on a file that's already globbed (Include would add a duplicate). PreserveNewest copies only when the source is newer; Always copies on every build and breaks incremental builds, so avoid it.

Embedding

<ItemGroup>
  <EmbeddedResource Include="Sql/*.sql" />
  <EmbeddedResource Include="Templates/receipt.html" LogicalName="Orders.Templates.Receipt" />
</ItemGroup>

Naming: without LogicalName, the manifest name is RootNamespace + relative folder path + file name, with path separators turned into dots: Sql/CreateOrders.sql in a project with root namespace Orders.Data becomes Orders.Data.Sql.CreateOrders.sql. Renaming a folder or the namespace changes the name, so explicit LogicalNames are safer for anything code looks up by name.

Reading:

await using var stream = typeof(ReceiptRenderer).Assembly
    .GetManifestResourceStream("Orders.Templates.Receipt")
    ?? throw new InvalidOperationException("Receipt template not embedded");

Assembly.GetManifestResourceNames() lists every embedded resource, which is the fastest way to debug a null stream.

.resx and localisation

Strings.resx plus Strings.en-AU.resx are embedded resources. The build compiles culture-specific ones into satellite assemblies (en-AU/Orders.resources.dll), and ResourceManager or ASP.NET Core's IStringLocalizer picks the right one by culture.

When to embed versus copy

Embed Copy to output
Small, read-only, versioned with the code (SQL scripts, JSON schemas, email templates, test fixtures) Large files, or files operators may change (configuration, certificates, data files)
Must not be missing or edited in production Should be replaceable without rebuilding

Production example

A reporting service read SQL from .sql files copied to the output folder. A deployment script that copied only DLLs and appsettings.json left the SQL behind, and reports failed in production. Embedding the scripts with explicit LogicalNames made the assembly self-contained, and a unit test that loads every expected resource name now catches renames.

Best practices, performance and security

  • Embedded resources increase assembly size and are loaded into memory when read; keep them small.
  • Never embed secrets or certificates: anyone with the DLL can extract them.
  • Prefer PreserveNewest over Always.
  • Add a test that asserts required resources exist.

🔍 What the interviewer is checking

Checks practical knowledge of how non-code files get into a build.

Green flags: knows the item types and copy metadata; explains manifest naming and LogicalName; reads resources correctly; can justify embed versus copy.

Red flags: can't explain a null resource stream; uses Always everywhere; embeds secrets.

📚 Learn in depth

1. How files flow through the build

project folder
|-- Program.cs              -> Compile          -> csc -> Orders.dll (IL)
|-- Sql/CreateOrders.sql    -> EmbeddedResource -> csc /resource -> inside Orders.dll
|-- Strings.resx            -> EmbeddedResource -> GenerateResource -> Orders.dll (neutral culture)
|-- Strings.en-AU.resx      -> EmbeddedResource -> GenerateResource -> en-AU/Orders.resources.dll (satellite)
|-- data/postcodes.csv      -> None + CopyToOutputDirectory -> bin/.../data/postcodes.csv
|-- wwwroot/site.css        -> Content (Web SDK) -> bin/... and publish/wwwroot/
`-- notes.md                -> None (no metadata) -> nowhere

Run time:
  typeof(X).Assembly.GetManifestResourceStream("Orders.Data.Sql.CreateOrders.sql")
  File.OpenRead(Path.Combine(AppContext.BaseDirectory, "data", "postcodes.csv"))

Every file in the project folder becomes some item, because SDK-style projects glob by default. The item type decides what happens to it.

2. Item types and their metadata

Item Default include Build result Key metadata
Compile **/*.cs Compiled into the assembly Link (for files outside the folder)
EmbeddedResource **/*.resx Stored inside the assembly LogicalName, WithCulture, DependentUpon
None Everything else Nothing, unless told to copy CopyToOutputDirectory, CopyToPublishDirectory, Link
Content Web SDK: wwwroot/**, **/*.json, **/*.config, Razor files Copied to output and publish Same copy metadata, plus ExcludeFromSingleFile

Copy metadata

Value Effect
Never (default for None) Not copied
PreserveNewest Copied when the source is newer than the destination
Always Copied on every build; marks the project as never up to date in Visual Studio's fast up-to-date check

CopyToOutputDirectory controls bin/; CopyToPublishDirectory controls publish/. Files copied to the output are normally published too.

Files outside the project folder

<ItemGroup>
  <None Include="..\..\shared\countries.json" Link="data\countries.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Link sets where the file appears in the project tree and in the output.

Copied files flow to referencing projects

Files a project copies to its output are also copied into the output of projects that reference it. A test project referencing Orders.Reporting gets data/postcodes.csv too. That's usually what you want, but it can surprise you when two referenced projects copy files with the same relative path: the last one copied wins.

3. Embedded resource names

Without LogicalName, the manifest name is built like this:

RootNamespace   +  relative folder path (separators -> dots)  +  file name
Orders.Data     .  Sql                                          .  CreateOrders.sql
=> "Orders.Data.Sql.CreateOrders.sql"
Detail Effect
Folder names that aren't valid identifiers Are adjusted (for example spaces become underscores), but the file name isn't
Renaming a folder or RootNamespace Changes every default name, and lookups silently return null
.resx next to a .cs file with a matching class Named after that class's namespace and name (the DependentUpon convention, on by default)
LogicalName="Orders.Templates.Receipt" Exactly that name, regardless of folders
LogicalName="Orders.Sql.%(Filename)" on a glob A stable naming scheme for a whole folder

The fastest debugging step for a null stream:

foreach (var name in typeof(Program).Assembly.GetManifestResourceNames())
    Console.WriteLine(name);

4. Reading resources

API Use for
Assembly.GetManifestResourceStream(name) Any embedded file; returns null if the name doesn't match
Assembly.GetManifestResourceNames() Listing and diagnostics
ManifestEmbeddedFileProvider (package Microsoft.Extensions.FileProviders.Embedded, with GenerateEmbeddedFilesManifest=true) Treat embedded files as a folder tree, with the original paths; useful for templates and static files
ResourceManager, IStringLocalizer<T> Localised strings from .resx

The stream is a view over the loaded assembly image. The real cost is what you build from it, so read once and cache:

private static readonly Lazy<string> ReceiptTemplate = new(() =>
{
    using var stream = typeof(ReceiptRenderer).Assembly.GetManifestResourceStream("Orders.Templates.Receipt")
        ?? throw new InvalidOperationException("Receipt template not embedded.");
    using var reader = new StreamReader(stream);
    return reader.ReadToEnd();
});

5. .resx, satellites and localisation

Strings.resx         -> neutral strings, inside Orders.dll
Strings.en-AU.resx   -> en-AU/Orders.resources.dll
Strings.fr.resx      -> fr/Orders.resources.dll

ResourceManager looks up: en-AU -> en -> neutral (inside the main assembly)
  • Set NeutralResourcesLanguage (for example en-AU) so lookups for the default culture don't search for a satellite that doesn't exist.
  • Packages often ship satellites for dozens of languages. <SatelliteResourceLanguages>en</SatelliteResourceLanguages> keeps only the ones you need, which shrinks the output and container image noticeably.
  • In ASP.NET Core, IStringLocalizer<T> with AddLocalization() and request localisation middleware chooses the culture per request.

6. Single-file, trimming and AOT

Publish shape Embedded resources Copied files
Framework-dependent folder Work as normal Next to the app; find them with AppContext.BaseDirectory
Single-file Work as normal Stay next to the executable unless IncludeAllContentForSelfExtract=true; Assembly.Location is empty for bundled assemblies
Trimmed / Native AOT Kept with their assembly Unaffected

Use AppContext.BaseDirectory to locate copied files in every shape; Assembly.Location and Directory.GetCurrentDirectory() are wrong for single-file apps and for services started from another folder.

7. Embed or copy?

Question Embed Copy
Must it ship with exactly this version of the code? Yes: SQL scripts, schemas, templates, test fixtures
Should operators change it without a rebuild? Yes: data files, optional configuration
Is it large (megabytes)? Yes, or move it to blob storage
Is it sensitive? No, never No: use Key Vault or a secret store
Is it localised text? .resx

8. Performance

  • Each embedded resource adds to the DLL, the container image and every deployment's download, even if the code that reads it never runs.
  • Reading is cheap; materialising (strings, parsed JSON, compiled templates) is the cost. Cache the result.
  • CopyToOutputDirectory="Always" makes Visual Studio treat the project as always out of date, so it rebuilds on every F5.
  • Trim unused satellite languages with SatelliteResourceLanguages.

9. Security

  • Anyone with the DLL can list and extract embedded resources (ILSpy shows them in seconds). Never embed secrets, private keys, certificates or connection strings.
  • Copied configuration files are just as visible on disk; secrets belong in Key Vault, managed identity or environment variables injected at run time.
  • Templates that render user data (emails, receipts) still need proper encoding; embedding doesn't make them safe.

10. .NET 11 awareness

Resource handling is stable. Watch the .NET 11 SDK notes for changes to single-file and AOT publishing defaults that affect copied files. Keep production on .NET 10 until you've tested an upgrade.

11. Try it yourself

  1. Add Sql/Hello.sql to a console project and mark it as EmbeddedResource.
  2. Print typeof(Program).Assembly.GetManifestResourceNames() and note the default name.
  3. Rename the Sql folder to Scripts and print the names again.
  4. Add LogicalName="Demo.Sql.%(Filename)", update the code, and rename the folder back; the name no longer changes.
  5. Add data/info.txt with CopyToOutputDirectory="PreserveNewest", build, and read it with AppContext.BaseDirectory.
  6. Reference the console project from a test project and check that data/info.txt appears in the test output.
  7. Publish with -p:PublishSingleFile=true -r linux-x64 and compare Assembly.Location with AppContext.BaseDirectory.
  8. Add a package with many satellite languages (for example Humanizer), build, count the culture folders, then set SatelliteResourceLanguages to en and count again.

💻 Code

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>Orders.Reporting</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <EmbeddedResource Include="Sql/**/*.sql" LogicalName="Orders.Reporting.Sql.%(Filename)" />
    <None Update="data/postcodes.csv" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
</Project>
using System.Reflection;

namespace Orders.Reporting;

public static class SqlScripts
{
    private static readonly Assembly Assembly = typeof(SqlScripts).Assembly;

    public static async Task<string> LoadAsync(string name, CancellationToken cancellationToken = default)
    {
        var resourceName = $"Orders.Reporting.Sql.{name}";
        await using var stream = Assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException(
                $"Embedded SQL '{resourceName}' not found. Available: {string.Join(", ", Assembly.GetManifestResourceNames())}");
        using var reader = new StreamReader(stream);
        return await reader.ReadToEndAsync(cancellationToken);
    }
}

// A test that fails if a script is renamed or not embedded
public class SqlScriptsTests
{
    [Theory]
    [InlineData("MonthlyGstSummary")]
    [InlineData("OutstandingInvoices")]
    public async Task Script_is_embedded(string name) =>
        Assert.False(string.IsNullOrWhiteSpace(await SqlScripts.LoadAsync(name)));
}

%(Filename) is item metadata without the extension, so Sql/MonthlyGstSummary.sql becomes Orders.Reporting.Sql.MonthlyGstSummary.

⚠ Common mistakes

  • Mistake: using Include on a file that is already globbed. <None Include="data/postcodes.csv" CopyToOutputDirectory="..." /> adds a second item instead of changing the first, causing duplicate-item errors or files copied twice. Instead: use Update to change metadata on globbed files, and Include only for files outside the default globs.

  • Mistake: guessing the manifest resource name. GetManifestResourceStream returns null when the name doesn't match exactly, and the failure appears later as a NullReferenceException. Instead: print GetManifestResourceNames() to see the real names, and throw a clear exception when a lookup returns null.

  • Mistake: relying on default resource names. Renaming a folder or changing RootNamespace changes every default name, so lookups break silently after an innocent refactor. Instead: set an explicit LogicalName (for example Orders.Sql.%(Filename) on a glob) and add a test that loads every resource the code needs.

  • Mistake: using CopyToOutputDirectory="Always". The file is copied on every build and Visual Studio treats the project as never up to date, slowing every build. Instead: use PreserveNewest unless you have a specific reason to copy every time.

  • Mistake: embedding secrets, keys or certificates. Anyone with the DLL can extract embedded resources in seconds with standard tools. Instead: load secrets at run time from Key Vault, managed identity or environment variables.

  • Mistake: embedding large files and reading them on every request. Assembly and image sizes grow, and each request allocates a large string or object graph. Instead: keep embedded assets small, read once and cache the result, and move large data to copied files or blob storage.

  • Mistake: finding copied files with Assembly.Location or the current directory. Assembly.Location is empty for single-file apps, and the current directory depends on how the process was started (services often start in C:\Windows\System32 or /). Instead: build paths from AppContext.BaseDirectory.

  • Mistake: forgetting that copied files flow to referencing projects. Two referenced projects that copy files with the same relative path overwrite each other in the consumer's output, and the app reads the wrong file. Instead: give copied files distinct paths per project, or embed assets that belong to one library.

  • Mistake: shipping every satellite language from packages. Dozens of culture folders bloat the output and container image for languages the app never uses. Instead: set SatelliteResourceLanguages to the cultures you support, for example en.

  • Mistake: relying on deployment scripts to copy loose files. A script that copies only DLLs and appsettings.json leaves SQL files or templates behind, and the feature fails only in production. Instead: embed assets the code can't run without, or deploy the whole publish folder, and add a start-up check that required files exist.

🏛 Senior insight: trade-offs

Choosing between embedding and copying is a deployment decision: embedded assets can't go missing or be edited, copied ones can be changed without a rebuild. A senior engineer makes that choice deliberately per asset, uses explicit resource names with a test that guards them, and keeps secrets out of both.

Size and performance. Embedded resources are part of the assembly image, so every one adds to DLL size, container image size and download time for every deployment, even if the code path that reads it never runs. Reading a resource returns a stream over the loaded image; the real cost is what you materialise from it, such as a large string or a parsed object graph. Seniors keep embedded assets small, read them once and cache the result (a static readonly or a lazily loaded singleton) instead of re-reading on every request, and move anything large (models, datasets, media) to copied files or blob storage. Copied files cost disk I/O on first read and need the same caching, plus a startup check that they exist.

For localisation they rely on .resx and satellite assemblies (or a translation service with a cache) rather than ad hoc file reading.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate uses %(Filename) or Update on items. Why: Checks the MSBuild concepts behind items and metadata.
    How to answer

    MSBuild is the build engine under dotnet build, Visual Studio and Rider, and a .csproj is an MSBuild file. Properties are named values such as $(Configuration). Items are lists with metadata, such as @(Compile) holding every .cs file. Targets are named steps, ordered with DependsOnTargets, BeforeTargets and AfterTargets. Each target runs tasks, the real units of work, such as Csc to compile or Copy. The build has two phases. Evaluation reads imports, properties and items. Execution runs the targets. Targets that declare Inputs and Outputs are skipped when nothing has changed, which is what makes builds incremental.

  2. When: Candidate mentions single-file publishing or Assembly.Location. Why: Single-file and AOT publishing change how copied files are found.
    How to answer

    By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.

  3. When: Candidate is unsure why a file is already included. Why: Checks understanding of default globbing in SDK-style projects.
    How to answer

    An SDK-style project begins with <Project Sdk="Microsoft.NET.Sdk">. That one attribute pulls in all the standard build logic, so the file only holds what's specific to your project: the target framework, the output type, a few settings and its references. Source files are picked up automatically by globbing, so you don't list each .cs file. Packages are PackageReference items. Old-style .NET Framework project files listed every file, imported targets by hand and used packages.config. That made them long, noisy in merges and tied to Visual Studio on Windows.

More in Build System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me