A Roslyn source generator that turns configured folder trees into strongly typed path constants at compile time. Each configured folder becomes a nested static class (for example Src, Template) so you can avoid magic strings.
Given a structure like:
/src
Template1.anyext
folderA/
Template2.anyext
folderB/
Template3.anyext
Template4.anyext
/template
email/
welcome.txt
sms/
otp.txt
the generator emits one file per root folder (TypedPaths.Src.g.cs, TypedPaths.Template.g.cs, ...), each defining a top-level static class in the TypedPaths namespace:
// TypedPaths.Src.g.cs
// <auto-generated/>
namespace TypedPaths;
public static class Src
{
public const string Value = "src";
public static class Template1
{
public const string Value = "src/Template1.anyext";
}
public static class FolderA
{
public const string Value = "src/folderA";
public static class Template2
{
public const string Value = "src/folderA/Template2.anyext";
}
}
public static class FolderB
{
public const string Value = "src/folderB";
public static class Template3
{
public const string Value = "src/folderB/Template3.anyext";
}
public static class Template4
{
public const string Value = "src/folderB/Template4.anyext";
}
}
}// TypedPaths.Template.g.cs
// <auto-generated/>
namespace TypedPaths;
public static class Template
{
public const string Value = "template";
public static class Email
{
public const string Value = "template/email";
public static class Welcome
{
public const string Value = "template/email/welcome.txt";
}
}
public static class Sms
{
public const string Value = "template/sms";
public static class Otp
{
public const string Value = "template/sms/otp.txt";
}
}
}With using TypedPaths; you can use Src.FolderA.Template2.Value and Template.Email.Welcome.Value instead of raw string paths.
- .NET 8 (or the TFM your project uses; the generator targets .NET Standard 2.0)
- MSBuild / SDK-style projects
<ItemGroup>
<PackageReference Include="TypedPaths.Generator" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<TypedPathsFolder Include="src" ClassName="Src" />
<TypedPathsFolder Include="template" />
</ItemGroup>That is the only configuration needed in consumer projects.
TypedPaths.Generator.targets (from package build) automatically maps each TypedPathsFolder to AdditionalFiles for source generation.
<ItemGroup>
<ProjectReference Include="..\TypedPaths.Generator\TypedPaths.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<TypedPathsFolder Include="src" ClassName="Src" />
<TypedPathsFolder Include="template" />
</ItemGroup>
<Import Project="..\TypedPaths.Generator\build\TypedPaths.Generator.targets"
Condition="Exists('..\TypedPaths.Generator\build\TypedPaths.Generator.targets')" />The explicit <Import /> is required only for local project-reference scenarios.
Build the project; the generator runs and adds TypedPaths.*.g.cs files to the compilation.
Add using TypedPaths; and use the generated classes directly. Each configured root folder becomes a top-level static class (e.g. Src, Template):
using TypedPaths;
// Always read .Value (works for both folder and file nodes)
string folderPath = Src.FolderA.Value; // "src/folderA"
string filePath = Src.FolderA.Template2.Value; // "src/folderA/Template2.anyext"
string emailTemplate = Template.Email.Welcome.Value; // "template/email/welcome.txt"
// e.g. resolve to full path
var fullPath = Path.Combine(projectRoot, Src.FolderA.Template2.Value);Value is always a path relative to the project root:
- Folder node
Value= relative folder path - File node
Value= relative file path (including extension)
Nested child classes are emitted only for folders (because only folders can contain files/subfolders).
- Folder and file names become PascalCase identifiers.
- Invalid identifier characters are dropped or split; leading digits get a
_prefix. - Duplicate names in the same scope get suffixes:
_2,_3, etc. - Extensions are stripped from member names but kept in the path string.
- If a folder name and file name conflict in the same scope:
- the folder keeps the base name;
- the file name becomes
FileName + ExtensionWithoutDot(example:report/+report.txt->ReportandReportTxt); - if the conflicting file has no extension, use
Filesuffix (example:data/+data->DataandDataFile).
| Project | Description |
|---|---|
TypedPaths.Generator |
The source generator (Roslyn incremental generator). |
TypedPaths.Generator.Sample |
Example app that uses the generator and runs a small demo. |
TypedPaths.Generator.Tests |
Unit tests for the generator. |
dotnet restore
dotnet build
dotnet testRun the sample:
dotnet run --project TypedPaths.Generator.SampleSee the repository for license information.