├── icon.png
├── tests
├── SetSharp.Tests
│ ├── Helpers
│ │ ├── SetSharpJsonParserTests.cs
│ │ └── SetSharpJsonReaderTests.cs
│ ├── SetSharp.Tests.csproj
│ ├── CodeGeneration
│ │ ├── PocoGeneratorTests.cs
│ │ └── OptionsPatternGeneratorTests.cs
│ └── ModelBuilder
│ │ └── ConfigurationModelBuilderTests.cs
└── SetSharp.Demo
│ ├── myCustomAppsettings.json
│ ├── SetSharp.Demo.csproj
│ └── Program.cs
├── src
└── SetSharp
│ ├── AnalyzerReleases.Unshipped.md
│ ├── build
│ └── SetSharp.props
│ ├── Models
│ ├── SetSharpSettings.cs
│ ├── SettingPropertyInfo.cs
│ ├── SettingClassInfo.cs
│ └── SourceGenerationModel.cs
│ ├── Definitions
│ └── MSBuildPropertyKeys.cs
│ ├── AnalyzerReleases.Shipped.md
│ ├── Providers
│ └── GeneratorSettingsProvider.cs
│ ├── Helpers
│ ├── SetSharpJsonReader.cs
│ └── SetSharpJsonParser.cs
│ ├── Diagnostics
│ └── DiagnosticDescriptors.cs
│ ├── SetSharp.csproj
│ ├── CodeGeneration
│ ├── PocoGenerator.cs
│ └── OptionPatternGenerator.cs
│ ├── ModelBuilder
│ └── ConfigurationModelBuilder.cs
│ └── SetSharpSourceGenerator.cs
├── LICENSE.txt
├── .github
└── workflows
│ └── publish.yml
├── .gitattributes
├── SetSharp.sln
├── CHANGELOG.md
├── .gitignore
└── README.md
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beheshty/SetSharp/HEAD/icon.png
--------------------------------------------------------------------------------
/tests/SetSharp.Tests/Helpers/SetSharpJsonParserTests.cs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beheshty/SetSharp/HEAD/tests/SetSharp.Tests/Helpers/SetSharpJsonParserTests.cs
--------------------------------------------------------------------------------
/src/SetSharp/AnalyzerReleases.Unshipped.md:
--------------------------------------------------------------------------------
1 | ; Unshipped analyzer release
2 | ; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3 |
--------------------------------------------------------------------------------
/src/SetSharp/build/SetSharp.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/SetSharp/Models/SetSharpSettings.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace SetSharp.Models
3 | {
4 | internal class SetSharpSettings
5 | {
6 | public bool OptionPatternGenerationEnabled { get; set; } = true;
7 | public string SourceFile { get; set; } = "appsettings.json";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/SetSharp/Models/SettingPropertyInfo.cs:
--------------------------------------------------------------------------------
1 | namespace SetSharp.Models
2 | {
3 | internal class SettingPropertyInfo
4 | {
5 | internal string PropertyType { get; set; }
6 | internal string PropertyName { get; set; }
7 | internal string OriginalJsonKey { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/SetSharp/Models/SettingClassInfo.cs:
--------------------------------------------------------------------------------
1 | namespace SetSharp.Models
2 | {
3 | internal class SettingClassInfo
4 | {
5 | internal string ClassName { get; set; }
6 | internal string SectionPath { get; set; }
7 | internal List Properties { get; set; } = [];
8 | internal bool IsFromCollection { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/SetSharp.Demo/myCustomAppsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*",
9 | "ConnectionStrings": {
10 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=test;Trusted_Connection=True;"
11 | },
12 | "FeatureFlags": [
13 | {
14 | "Name": "UseNewDashboard",
15 | "IsEnabled": true
16 | },
17 | {
18 | "Name": "EnableExperimentalApi",
19 | "IsEnabled": false
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/src/SetSharp/Definitions/MSBuildPropertyKeys.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace SetSharp.Definitions
6 | {
7 | internal static class MSBuildPropertyKeys
8 | {
9 | private const string _prefix = "build_property.";
10 |
11 | public const string SetSharpPrefix = "SetSharp_";
12 |
13 | public const string OptionPatternGenerationEnabled = _prefix + SetSharpPrefix + "OptionPatternGenerationEnabled";
14 |
15 | public const string SourceFile = _prefix + SetSharpPrefix + "SourceFile";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/SetSharp/Models/SourceGenerationModel.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace SetSharp.Models
4 | {
5 | internal class SourceGenerationModel
6 | {
7 | internal SourceGenerationModel(List? classes,
8 | SetSharpSettings setSharpSettings,
9 | Diagnostic? diagnostic)
10 | {
11 | Classes = classes;
12 | SetSharpSettings = setSharpSettings;
13 | Diagnostic = diagnostic;
14 | }
15 |
16 | internal List? Classes { get; }
17 | internal SetSharpSettings SetSharpSettings { get; }
18 | internal Diagnostic? Diagnostic { get; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/SetSharp/AnalyzerReleases.Shipped.md:
--------------------------------------------------------------------------------
1 | ; Shipped analyzer release
2 |
3 | ### 3.0.1
4 |
5 | Rule ID | Category | Severity | Notes
6 | --------|----------|----------|-------
7 | SSG001 | Error | Error | App settings parsing failed. Triggered when internal parsing logic throws or fails due to invalid structure or unsupported types.
8 | SSG002 | Usage | Error | Missing reference to 'Microsoft.Extensions.Options.ConfigurationExtensions' required for generating IOptions extensions.
9 | SSG003 | Usage | Error | Missing reference(s) to 'Microsoft.Extensions.Configuration.Abstractions' and/or 'System.Collections.Immutable' required for basic functionality.
10 | SSG004 | Configuration | Warning | The source file specified via MSBuild property was not found as an 'AdditionalFile' in the project.
11 |
--------------------------------------------------------------------------------
/tests/SetSharp.Tests/SetSharp.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/SetSharp.Demo/SetSharp.Demo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 | PreserveNewest
13 |
14 |
15 |
16 |
17 | true
18 | myCustomAppsettings.json
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Amirhossein Beheshti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/SetSharp/Providers/GeneratorSettingsProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.Diagnostics;
3 | using SetSharp.Definitions;
4 | using SetSharp.Models;
5 |
6 | namespace SetSharp.Providers
7 | {
8 | internal class GeneratorSettingsProvider
9 | {
10 | internal static IncrementalValueProvider GetSettings(IncrementalGeneratorInitializationContext context)
11 | {
12 | return context.AnalyzerConfigOptionsProvider.Select(ParseSettings);
13 | }
14 |
15 | internal static SetSharpSettings ParseSettings(AnalyzerConfigOptionsProvider provider, CancellationToken cancellationToken)
16 | {
17 | var settings = new SetSharpSettings();
18 |
19 | if (provider.GlobalOptions.TryGetValue(MSBuildPropertyKeys.OptionPatternGenerationEnabled, out var enabledValue)
20 | && bool.TryParse(enabledValue, out var parsedBool))
21 | {
22 | settings.OptionPatternGenerationEnabled = parsedBool;
23 | }
24 |
25 | if (provider.GlobalOptions.TryGetValue(MSBuildPropertyKeys.SourceFile, out var sourceFileValue)
26 | && !string.IsNullOrWhiteSpace(sourceFileValue))
27 | {
28 | settings.SourceFile = sourceFileValue;
29 | }
30 |
31 | return settings;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/SetSharp/Helpers/SetSharpJsonReader.cs:
--------------------------------------------------------------------------------
1 |
2 |
3 | namespace SetSharp.Helpers
4 | {
5 | internal static class SetSharpJsonReader
6 | {
7 | ///
8 | /// Reads a value from a nested dictionary using a colon-delimited key path.
9 | ///
10 | /// The top-level dictionary representing the parsed JSON.
11 | /// The path to the desired value (e.g., "SetSharp:OptionPatternGenerationEnabled").
12 | /// The found value as an object, or null if the path is invalid or the key is not found.
13 | internal static object? Read(Dictionary json, string keyPath)
14 | {
15 | if (json == null || string.IsNullOrWhiteSpace(keyPath))
16 | {
17 | return null;
18 | }
19 |
20 | string[] keys = keyPath.Split(':');
21 | object currentNode = json;
22 |
23 | foreach (var key in keys)
24 | {
25 | if (currentNode is not Dictionary currentDict)
26 | {
27 | return null;
28 | }
29 |
30 | if (!currentDict.TryGetValue(key, out currentNode))
31 | {
32 | return null;
33 | }
34 | }
35 |
36 | return currentNode;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/SetSharp/Diagnostics/DiagnosticDescriptors.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace SetSharp.Diagnostics
4 | {
5 | internal static class DiagnosticDescriptors
6 | {
7 | internal static readonly DiagnosticDescriptor ParsingFailedError = new(
8 | "SSG001",
9 | "App settings parsing failed",
10 | "{0}",
11 | "Error",
12 | DiagnosticSeverity.Error,
13 | true);
14 |
15 | internal static readonly DiagnosticDescriptor MissingOptionsDependencyError = new(
16 | "SSG002",
17 | "Missing Dependencies",
18 | "To generate IOptions extensions, the project must reference 'Microsoft.Extensions.Options.ConfigurationExtensions'. Please install the corresponding NuGet package.",
19 | "Usage",
20 | DiagnosticSeverity.Error,
21 | true);
22 |
23 | internal static readonly DiagnosticDescriptor MissingBaseDependencyError = new(
24 | "SSG003",
25 | "Missing Dependency",
26 | "The project must reference 'Microsoft.Extensions.Configuration.Abstractions' and 'System.Collections.Immutable' for basic functionality. Please install the corresponding NuGet package.",
27 | "Usage",
28 | DiagnosticSeverity.Error,
29 | true);
30 |
31 | internal static readonly DiagnosticDescriptor SourceFileNotFoundWarning = new(
32 | "SSG004",
33 | "Source File Not Found",
34 | "The specified source file '{0}' was not found for assembly '{1}'. Ensure the file is included in your project as an 'AdditionalFiles' item in the .csproj.",
35 | "Configuration",
36 | DiagnosticSeverity.Warning,
37 | true);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish NuGet Package
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v4
15 |
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version: '9.0.x'
20 |
21 | - name: Restore dependencies
22 | run: dotnet restore
23 |
24 | - name: Get the version from the .csproj file
25 | id: get_version
26 | run: |
27 | VERSION=$(grep -oPm1 "(?<=)[^<]+" src/SetSharp/SetSharp.csproj)
28 | echo "VERSION=$VERSION" >> $GITHUB_ENV
29 |
30 | - name: Get the latest published version from NuGet
31 | id: get_latest_version
32 | run: |
33 | PACKAGE_ID="SetSharp"
34 | LATEST_VERSION=$(curl -s https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/index.json | jq -r '.versions | last')
35 | echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
36 |
37 | - name: Compare versions
38 | id: version_check
39 | run: |
40 | if [ "$VERSION" != "$LATEST_VERSION" ]; then
41 | echo "New version detected: $VERSION"
42 | echo "run_publish=true" >> $GITHUB_ENV
43 | else
44 | echo "No new version detected"
45 | echo "run_publish=false" >> $GITHUB_ENV
46 | fi
47 |
48 | - name: Build
49 | if: env.run_publish == 'true'
50 | run: dotnet build --configuration Release --no-restore
51 |
52 | - name: Pack
53 | if: env.run_publish == 'true'
54 | run: dotnet pack src/SetSharp/SetSharp.csproj --configuration Release --no-build
55 |
56 | - name: Publish to NuGet
57 | if: env.run_publish == 'true'
58 | run: dotnet nuget push "src/SetSharp/bin/Release/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source "https://api.nuget.org/v3/index.json"
59 |
--------------------------------------------------------------------------------
/tests/SetSharp.Demo/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Options;
5 | using SetSharp.Configuration;
6 |
7 |
8 | Console.WriteLine("Configuration Explorer is live! Let's see what secrets your appsettings hold...\n");
9 |
10 | var builder = Host.CreateApplicationBuilder(args);
11 | builder.Configuration.Sources.Clear();
12 | builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
13 |
14 | // --- Register All Your Settings with a Single Call ---
15 | builder.Services.AddAllGeneratedOptions(builder.Configuration);
16 |
17 | using var host = builder.Build();
18 | // --- Logging Options ---
19 | var loggingOptions = host.Services.GetRequiredService>().Value;
20 | if (loggingOptions is not null)
21 | {
22 | Console.WriteLine("Logging Options:");
23 | Console.WriteLine($" • {nameof(loggingOptions.Default)}: {loggingOptions.Default}");
24 | Console.WriteLine($" • {nameof(loggingOptions.MicrosoftAspNetCore)}: {loggingOptions.MicrosoftAspNetCore}");
25 | Console.WriteLine();
26 | }
27 |
28 | // --- Connection Strings ---
29 | var connectionOptions = host.Services.GetRequiredService>().Value;
30 | if (connectionOptions is not null)
31 | {
32 | Console.WriteLine("Connection Strings:");
33 | Console.WriteLine($" • {nameof(connectionOptions.DefaultConnection)}: {connectionOptions.DefaultConnection}");
34 | Console.WriteLine();
35 | }
36 |
37 | // --- Feature Flags ---
38 | var featureFlagOptions = host.Services.GetRequiredService>>().Value;
39 | if (featureFlagOptions is not null && featureFlagOptions.Count != 0)
40 | {
41 | Console.WriteLine("Feature Flag Options:");
42 | for (int i = 0; i < featureFlagOptions.Count; i++)
43 | {
44 | var op = featureFlagOptions[i];
45 | Console.WriteLine($" {i + 1}.");
46 | Console.WriteLine($" • {nameof(op.Name)}: {op.Name}");
47 | Console.WriteLine($" • {nameof(op.IsEnabled)}: {op.IsEnabled}");
48 | Console.WriteLine();
49 | }
50 | }
51 |
52 | Console.WriteLine("All configuration values displayed. Press any key to exit...");
53 | Console.ReadKey();
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/SetSharp/SetSharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | false
6 | enable
7 | enable
8 | true
9 | Latest
10 | true
11 |
12 | SetSharp
13 | 3.1.0
14 | icon.png
15 | Amirhossein Beheshti
16 | Generates strongly typed settings classes from appsettings.json using Source Generators.
17 | README.md
18 | source-generator configuration appsettings csharp
19 | MIT
20 | https://github.com/beheshty/SharpSettings
21 | https://github.com/beheshty/SharpSettings
22 | git
23 |
24 |
25 |
26 |
27 | all
28 | runtime; build; native; contentfiles; analyzers
29 |
30 |
31 | all
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/SetSharp.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.14.36221.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F50C554B-27D0-43FA-9F06-86443CE8C203}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetSharp", "src\SetSharp\SetSharp.csproj", "{4530ED82-BD77-BD0C-42CD-31D782027653}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetSharp.Demo", "tests\SetSharp.Demo\SetSharp.Demo.csproj", "{AC4C9F1A-4ED9-2828-78DC-7F763C084010}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetSharp.Tests", "tests\SetSharp.Tests\SetSharp.Tests.csproj", "{EAD63588-C89D-5E4D-B276-880FB86E519C}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {4530ED82-BD77-BD0C-42CD-31D782027653}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {4530ED82-BD77-BD0C-42CD-31D782027653}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {4530ED82-BD77-BD0C-42CD-31D782027653}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {4530ED82-BD77-BD0C-42CD-31D782027653}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {AC4C9F1A-4ED9-2828-78DC-7F763C084010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {AC4C9F1A-4ED9-2828-78DC-7F763C084010}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {AC4C9F1A-4ED9-2828-78DC-7F763C084010}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {AC4C9F1A-4ED9-2828-78DC-7F763C084010}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {EAD63588-C89D-5E4D-B276-880FB86E519C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {EAD63588-C89D-5E4D-B276-880FB86E519C}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {EAD63588-C89D-5E4D-B276-880FB86E519C}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {EAD63588-C89D-5E4D-B276-880FB86E519C}.Release|Any CPU.Build.0 = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(NestedProjects) = preSolution
39 | {4530ED82-BD77-BD0C-42CD-31D782027653} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
40 | {AC4C9F1A-4ED9-2828-78DC-7F763C084010} = {F50C554B-27D0-43FA-9F06-86443CE8C203}
41 | {EAD63588-C89D-5E4D-B276-880FB86E519C} = {F50C554B-27D0-43FA-9F06-86443CE8C203}
42 | EndGlobalSection
43 | GlobalSection(ExtensibilityGlobals) = postSolution
44 | SolutionGuid = {A2ADCB9C-D71A-452C-9B2B-CEABA5277F66}
45 | EndGlobalSection
46 | EndGlobal
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [3.1.0] - 2025-09-26
9 | ### Changed
10 | - The diagnostic for a missing source file has been changed from a build-halting error to a non-blocking warning. The generator now continues without generating code instead of failing the compilation.
11 | - The warning for a missing source file now includes the name of the calling assembly to provide better context in multi-project solutions.
12 |
13 | ## [3.0.0] - 2025-08-19
14 | ### Breaking Change
15 | - **Generator configuration has been moved from `appsettings.json` to MSBuild properties in the `.csproj` file.** The `SetSharp` section within `appsettings.json` is no longer read. Users upgrading to this version **must** migrate their settings to their project file to continue configuring the generator.
16 |
17 | ### Added
18 | - A new MSBuild property, `SetSharp_SourceFile`, allows you to specify a custom JSON file for generation, replacing the default `appsettings.json`.
19 | - The `SetSharp_OptionPatternGenerationEnabled` MSBuild property now controls the generation of `IOptions` extension methods.
20 | - A new diagnostic (`SSG004`) will now report an error if the specified source file cannot be found in the project's `AdditionalFiles`.
21 |
22 | ### Changed
23 | - Refactored settings logic into a dedicated `GeneratorSettingsProvider` class to separate concerns.
24 |
25 | ## [2.1.0] - 2025-08-10
26 | ### Changed
27 | - Enhanced the type inference logic for JSON arrays. The source generator now analyzes **all** objects within an array to create a single, comprehensive class for the list items. This replaces the previous behavior of only inspecting the first item, ensuring that properties from all objects are correctly captured.
28 |
29 | ## [2.0.1] - 2025-08-04
30 | ### Added
31 | - Added `Shipped.md` and `Unshipped.md` files to track analyzer diagnostics and releases. These files follow the official Roslyn analyzer release tracking format.
32 |
33 | ### Notes
34 | - This is a documentation-only update with no functional code changes.
35 |
36 |
37 | ## [2.0.0] - 2025-07-16
38 | ### Changed
39 | - **Breaking Change**: All generated classes are now `record` types instead of regular `class` types.
40 | - All properties use `init` accessors instead of `set` to support immutability.
41 | - Collections are now generated as `ImmutableList` instead of mutable `List`.
42 |
43 | ### Notes
44 | - These changes improve immutability and align with modern C# practices.
45 | - This is a major version bump due to potential breaking changes in downstream usage, especially if consumers relied on mutable types or reflection-based assumptions.
46 |
47 | ## [1.3.0] - 2025-07-13
48 | ### Added
49 | - Descriptive error messages if required dependencies are missing.
50 | - New diagnostic IDs: `SSG001`, `SSG002`, `SSG003`.
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/SetSharp/CodeGeneration/PocoGenerator.cs:
--------------------------------------------------------------------------------
1 | using SetSharp.Models;
2 | using System.Reflection;
3 | using System.Text;
4 |
5 | namespace SetSharp.CodeGeneration
6 | {
7 | internal static class PocoGenerator
8 | {
9 | internal static string Generate(List classes, string @namespace = "SetSharp.Configuration")
10 | {
11 | var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
12 | var sb = new StringBuilder();
13 |
14 | sb.AppendLine("// ");
15 | sb.AppendLine("#nullable enable");
16 | sb.AppendLine();
17 | sb.AppendLine("using System.Collections.Generic;");
18 | sb.AppendLine("using System.Collections.Immutable;");
19 | sb.AppendLine();
20 | sb.AppendLine($"namespace {@namespace}");
21 | sb.AppendLine("{");
22 |
23 | foreach (var SettingClassInfo in classes)
24 | {
25 | sb.AppendLine();
26 | AddClassSummary(sb, SettingClassInfo);
27 | sb.AppendLine($" [System.CodeDom.Compiler.GeneratedCode(\"SetSharp\", \"{assemblyVersion}\")]");
28 | sb.AppendLine($" public partial record {SettingClassInfo.ClassName}");
29 | sb.AppendLine(" {");
30 |
31 | AddSectionNameConstant(sb, SettingClassInfo);
32 |
33 | foreach (var property in SettingClassInfo.Properties)
34 | {
35 | AddProperty(sb, property);
36 | }
37 |
38 | sb.AppendLine(" }");
39 | }
40 |
41 | sb.AppendLine("}");
42 | return sb.ToString();
43 | }
44 |
45 | private static void AddClassSummary(StringBuilder sb, SettingClassInfo SettingClassInfo)
46 | {
47 | if (!string.IsNullOrEmpty(SettingClassInfo.SectionPath))
48 | {
49 | sb.AppendLine($" /// Represents the '{SettingClassInfo.SectionPath}' section from the configuration.");
50 | }
51 | else
52 | {
53 | sb.AppendLine(" /// Represents the root of the configuration settings.");
54 | }
55 | }
56 |
57 | private static void AddSectionNameConstant(StringBuilder sb, SettingClassInfo SettingClassInfo)
58 | {
59 | if (!string.IsNullOrEmpty(SettingClassInfo.SectionPath))
60 | {
61 | sb.AppendLine($" /// The configuration section name for this class: \"{SettingClassInfo.SectionPath}\"");
62 | sb.AppendLine($" public const string SectionName = \"{SettingClassInfo.SectionPath}\";");
63 | sb.AppendLine();
64 | }
65 | }
66 |
67 | private static void AddProperty(StringBuilder sb, SettingPropertyInfo property)
68 | {
69 | sb.AppendLine($" /// Maps to the '{property.OriginalJsonKey}' configuration key.");
70 | if (property.PropertyName != property.OriginalJsonKey)
71 | {
72 | sb.AppendLine($" [Microsoft.Extensions.Configuration.ConfigurationKeyName(\"{property.OriginalJsonKey}\")]");
73 | }
74 | sb.AppendLine($" public {property.PropertyType} {property.PropertyName} {{ get; init; }}");
75 | sb.AppendLine();
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/SetSharp/CodeGeneration/OptionPatternGenerator.cs:
--------------------------------------------------------------------------------
1 | using SetSharp.Models;
2 | using System.Reflection;
3 | using System.Text;
4 |
5 | namespace SetSharp.CodeGeneration
6 | {
7 | internal static class OptionsPatternGenerator
8 | {
9 | internal static string Generate(List classes)
10 | {
11 | var pocoNameSpace = "SetSharp.Configuration";
12 | var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
13 |
14 | var sb = new StringBuilder();
15 | sb.AppendLine("// ");
16 | sb.AppendLine("using Microsoft.Extensions.Configuration;");
17 | sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
18 | sb.AppendLine($"using {pocoNameSpace};");
19 | sb.AppendLine();
20 | sb.AppendLine("namespace Microsoft.Extensions.DependencyInjection");
21 | sb.AppendLine("{");
22 | sb.AppendLine($" [System.CodeDom.Compiler.GeneratedCode(\"SetSharp\", \"{assemblyVersion}\")]");
23 | sb.AppendLine(" public static class GeneratedOptionsExtensions");
24 | sb.AppendLine(" {");
25 |
26 | var optionClasses = classes.Where(c => !string.IsNullOrEmpty(c.SectionPath)).ToList();
27 |
28 | AppendAddOptionMethods(sb, optionClasses);
29 |
30 | if (optionClasses.Any())
31 | {
32 | AppendAddAllOptionsMethod(sb, optionClasses);
33 | }
34 |
35 | sb.AppendLine(" }");
36 | sb.AppendLine("}");
37 |
38 | return sb.ToString();
39 | }
40 |
41 | private static void AppendAddAllOptionsMethod(StringBuilder sb, List optionClasses)
42 | {
43 | sb.AppendLine(" /// Registers all generated configuration classes with the dependency injection container.");
44 | sb.AppendLine(" public static IServiceCollection AddAllGeneratedOptions(this IServiceCollection services, IConfiguration configuration)");
45 | sb.AppendLine(" {");
46 | foreach (var classInfo in optionClasses)
47 | {
48 | sb.AppendLine($" services.Add{classInfo.ClassName}(configuration);");
49 | }
50 | sb.AppendLine(" return services;");
51 | sb.AppendLine(" }");
52 | }
53 |
54 | private static void AppendAddOptionMethods(StringBuilder sb, List optionClasses)
55 | {
56 | foreach (var classInfo in optionClasses)
57 | {
58 | string configurationType = classInfo.IsFromCollection
59 | ? $"System.Collections.Generic.List<{classInfo.ClassName}>"
60 | : classInfo.ClassName;
61 |
62 | sb.AppendLine($" /// Registers the class with the dependency injection container.");
63 | sb.AppendLine($" public static IServiceCollection Add{classInfo.ClassName}(this IServiceCollection services, IConfiguration configuration)");
64 | sb.AppendLine(" {");
65 |
66 | sb.AppendLine($" services.Configure<{configurationType}>(configuration.GetSection({classInfo.ClassName}.SectionName));");
67 |
68 | sb.AppendLine(" return services;");
69 | sb.AppendLine(" }");
70 | sb.AppendLine();
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/SetSharp.Tests/CodeGeneration/PocoGeneratorTests.cs:
--------------------------------------------------------------------------------
1 | using SetSharp.CodeGeneration;
2 | using SetSharp.Models;
3 | using System.Reflection;
4 |
5 | namespace SetSharp.Tests.CodeGeneration
6 | {
7 | public class PocoGeneratorTests
8 | {
9 | [Fact]
10 | public void Generate_WithCustomNamespace_ShouldUseCustomNamespace()
11 | {
12 | // Arrange
13 | var classes = new List
14 | {
15 | new SettingClassInfo { ClassName = "MySettings" }
16 | };
17 | var customNamespace = "My.Custom.Namespace";
18 |
19 | // Act
20 | var result = PocoGenerator.Generate(classes, customNamespace);
21 |
22 | // Assert
23 | Assert.Contains($"namespace {customNamespace}", result);
24 | }
25 |
26 | [Fact]
27 | public void Generate_WithMultipleClasses_ShouldGenerateAllClasses()
28 | {
29 | // Arrange
30 | var classes = new List
31 | {
32 | new SettingClassInfo { ClassName = "FirstSettings" },
33 | new SettingClassInfo { ClassName = "SecondSettings" }
34 | };
35 |
36 | // Act
37 | var result = PocoGenerator.Generate(classes);
38 |
39 | // Assert
40 | Assert.Contains("public partial record FirstSettings", result);
41 | Assert.Contains("public partial record SecondSettings", result);
42 | }
43 |
44 | [Fact]
45 | public void Generate_WithDifferentPropertyAndJsonKey_ShouldAddConfigurationKeyNameAttribute()
46 | {
47 | // Arrange
48 | var classes = new List
49 | {
50 | new SettingClassInfo
51 | {
52 | ClassName = "MySettings",
53 | Properties = new List
54 | {
55 | new SettingPropertyInfo { PropertyName = "LogLevel", PropertyType = "string", OriginalJsonKey = "Logging:LogLevel:Default" }
56 | }
57 | }
58 | };
59 |
60 | // Act
61 | var result = PocoGenerator.Generate(classes);
62 |
63 | // Assert
64 | Assert.Contains("[Microsoft.Extensions.Configuration.ConfigurationKeyName(\"Logging:LogLevel:Default\")]", result);
65 | Assert.Contains("public string LogLevel { get; init; }", result);
66 | }
67 |
68 | [Fact]
69 | public void Generate_WithNoSectionPath_ShouldGenerateRootClassSummaryAndNoSectionNameConstant()
70 | {
71 | // Arrange
72 | var classes = new List
73 | {
74 | new SettingClassInfo
75 | {
76 | ClassName = "RootSettings",
77 | SectionPath = "" // or null
78 | }
79 | };
80 |
81 | // Act
82 | var result = PocoGenerator.Generate(classes);
83 |
84 | // Assert
85 | Assert.Contains("/// Represents the root of the configuration settings.", result);
86 | Assert.DoesNotContain("public const string SectionName", result);
87 | }
88 |
89 | [Fact]
90 | public void Generate_WithEmptyClassList_ShouldGenerateEmptyNamespace()
91 | {
92 | // Arrange
93 | var classes = new List();
94 | var expected = @"//
95 | #nullable enable
96 |
97 | using System.Collections.Generic;
98 | using System.Collections.Immutable;
99 |
100 | namespace SetSharp.Configuration
101 | {
102 | }
103 | ";
104 |
105 | // Act
106 | var result = PocoGenerator.Generate(classes);
107 |
108 | // Assert
109 | Assert.Equal(expected, result);
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/SetSharp.Tests/Helpers/SetSharpJsonReaderTests.cs:
--------------------------------------------------------------------------------
1 | using SetSharp.Helpers;
2 |
3 | namespace SetSharp.Tests.Helpers
4 | {
5 | public class SetSharpJsonReaderTests
6 | {
7 | private readonly Dictionary _testJson = new()
8 | {
9 | { "TopLevelString", "Hello World" },
10 | { "TopLevelInt", 123 },
11 | { "SetSharp", new Dictionary
12 | {
13 | { "Enabled", true },
14 | { "Generation", new Dictionary
15 | {
16 | { "Poco", true },
17 | { "OptionsPattern", false }
18 | }
19 | }
20 | }
21 | }
22 | };
23 |
24 | [Fact]
25 | public void Read_WithValidTopLevelPath_ReturnsCorrectValue()
26 | {
27 | // Arrange
28 | var keyPath = "TopLevelString";
29 |
30 | // Act
31 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
32 |
33 | // Assert
34 | Assert.Equal("Hello World", result);
35 | }
36 |
37 | [Fact]
38 | public void Read_WithValidNestedPath_ReturnsCorrectValue()
39 | {
40 | // Arrange
41 | var keyPath = "SetSharp:Enabled";
42 |
43 | // Act
44 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
45 |
46 | // Assert
47 | Assert.IsType(result);
48 | Assert.Equal(true, result);
49 | }
50 |
51 | [Fact]
52 | public void Read_WithDeeplyNestedPath_ReturnsCorrectValue()
53 | {
54 | // Arrange
55 | var keyPath = "SetSharp:Generation:Poco";
56 |
57 | // Act
58 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
59 |
60 | // Assert
61 | Assert.IsType(result);
62 | Assert.Equal(true, result);
63 | }
64 |
65 | [Fact]
66 | public void Read_PathToADictionary_ReturnsDictionaryObject()
67 | {
68 | // Arrange
69 | var keyPath = "SetSharp:Generation";
70 |
71 | // Act
72 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
73 |
74 | // Assert
75 | var dictResult = Assert.IsType>(result);
76 | Assert.True((bool)dictResult["Poco"]);
77 | Assert.False((bool)dictResult["OptionsPattern"]);
78 | }
79 |
80 | [Fact]
81 | public void Read_WithNonExistentTopLevelKey_ReturnsNull()
82 | {
83 | // Arrange
84 | var keyPath = "NonExistent";
85 |
86 | // Act
87 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
88 |
89 | // Assert
90 | Assert.Null(result);
91 | }
92 |
93 | [Fact]
94 | public void Read_WithNonExistentNestedKey_ReturnsNull()
95 | {
96 | // Arrange
97 | var keyPath = "SetSharp:Generation:NonExistent";
98 |
99 | // Act
100 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
101 |
102 | // Assert
103 | Assert.Null(result);
104 | }
105 |
106 | [Fact]
107 | public void Read_PathGoesThroughLeafValue_ReturnsNull()
108 | {
109 | // Arrange
110 | // Trying to navigate deeper into "TopLevelString", which is not a dictionary
111 | var keyPath = "TopLevelString:Deeper";
112 |
113 | // Act
114 | var result = SetSharpJsonReader.Read(_testJson, keyPath);
115 |
116 | // Assert
117 | Assert.Null(result);
118 | }
119 |
120 | [Fact]
121 | public void Read_WithNullJson_ReturnsNull()
122 | {
123 | // Arrange
124 | Dictionary nullJson = null;
125 | var keyPath = "Any:Path";
126 |
127 | // Act
128 | var result = SetSharpJsonReader.Read(nullJson, keyPath);
129 |
130 | // Assert
131 | Assert.Null(result);
132 | }
133 |
134 | [Theory]
135 | [InlineData(null)]
136 | [InlineData("")]
137 | [InlineData(" ")]
138 | public void Read_WithInvalidKeyPath_ReturnsNull(string invalidKeyPath)
139 | {
140 | // Act
141 | var result = SetSharpJsonReader.Read(_testJson, invalidKeyPath);
142 |
143 | // Assert
144 | Assert.Null(result);
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/src/SetSharp/ModelBuilder/ConfigurationModelBuilder.cs:
--------------------------------------------------------------------------------
1 | using SetSharp.Models;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace SetSharp.ModelBuilder
5 | {
6 | internal class ConfigurationModelBuilder
7 | {
8 | private readonly List _completedModels = [];
9 |
10 | internal List BuildFrom(Dictionary root)
11 | {
12 | var rootModel = new SettingClassInfo { ClassName = "RootOptions", SectionPath = "" };
13 | ProcessObject(rootModel, root);
14 | _completedModels.Add(rootModel);
15 |
16 | return _completedModels;
17 | }
18 |
19 | private void ProcessObject(SettingClassInfo SettingClassInfo, Dictionary obj)
20 | {
21 | foreach (var item in obj)
22 | {
23 | var propertyModel = new SettingPropertyInfo
24 | {
25 | OriginalJsonKey = item.Key,
26 | PropertyName = NormalizeName(item.Key),
27 | PropertyType = InferType(SettingClassInfo, item.Key, item.Value)
28 | };
29 | SettingClassInfo.Properties.Add(propertyModel);
30 | }
31 | }
32 |
33 | private string InferType(SettingClassInfo parentClass, string key, object value)
34 | {
35 | string HandleNestedObject(Dictionary obj)
36 | {
37 | var sectionPath = string.IsNullOrEmpty(parentClass.SectionPath) ? key : $"{parentClass.SectionPath}:{key}";
38 | return CreateNestedClass(sectionPath, key, obj, isFromCollection: false);
39 | }
40 |
41 | return value switch
42 | {
43 | string => "string",
44 | int => "int",
45 | long => "long",
46 | double => "double",
47 | bool => "bool",
48 | Dictionary obj => HandleNestedObject(obj),
49 | List