From 48e6c47cdd7d7a97168c12fe041695b43abbc458 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:26:29 -0700 Subject: [PATCH 01/10] Add CLAUDE.md directive for .NET agent rules Ensures future sessions explicitly load the .NET-specific agent directives from the public MCP server rather than relying on passive detection. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ced8182 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +At the start of every session, read the resource `chris-simmons://public-mcp/agent/rules/dotnet` from MCP server `plugin:public-mcp-server:public-mcp` and apply the rules. From c29b09d873869cbc7023b4ba48bf716e102d33b9 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:28:38 -0700 Subject: [PATCH 02/10] Enforce Canonical Application Name conventions, add config factories (#28) CanonicalApplicationName now validates all four parts against the naming conventions in the reference wiki (long form: lower-case, numbers, underscores, starting with a letter; short form: 1-4 lower-case letters/numbers), instead of only checking the long-form values for null/whitespace. Add CanonicalApplicationName.FromConfiguration and ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration factories that read the four well-known netchris:application:* keys, centralizing logic that was previously duplicated in consumers like NetChris.AspNetCore.Server. Co-Authored-By: Claude Sonnet 5 --- .../ApplicationMetadataTests.cs | 48 +++-- .../CanonicalApplicationNameTests.cs | 165 ++++++++++++++++++ .../NetChris.Core.UnitTests.csproj | 1 + NetChris.Core/ApplicationMetadata.cs | 24 +++ NetChris.Core/CanonicalApplicationName.cs | 81 +++++++-- NetChris.Core/NetChris.Core.csproj | 1 + 6 files changed, 300 insertions(+), 20 deletions(-) create mode 100644 NetChris.Core.UnitTests/CanonicalApplicationNameTests.cs diff --git a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs index 42a3080..783d8d2 100644 --- a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs +++ b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Reflection; using FluentAssertions; +using Microsoft.Extensions.Configuration; using Xunit; namespace NetChris.Core.UnitTests; @@ -17,18 +19,18 @@ public ApplicationMetadataTests() _appMetadata = new ApplicationMetadata( thisAssembly, - "ExpectedAppAggregate", + "expected_app_aggregate", "eaa", - "ExpectedAppComponent", + "expected_app_component", "eac", thisAssembly.GetName().Name!, "UnitTestEnvironment"); _appMetadataWithJustAggregateAndEnvironment = ApplicationMetadata.GetApplicationMetadataFromEntryAssembly( - "ExpectedAppAggregate", + "expected_app_aggregate", "eaa", - "ExpectedAppComponent", + "expected_app_component", "eac", "UnitTestEnvironment"); } @@ -52,7 +54,7 @@ public void ApplicationAggregate_should_flow_through() var applicationAggregate = _appMetadata.CanonicalApplicationName.ApplicationAggregate; // Assert - applicationAggregate.Should().Be("ExpectedAppAggregate"); + applicationAggregate.Should().Be("expected_app_aggregate"); } [Fact] @@ -74,7 +76,7 @@ public void ApplicationComponent_should_flow_through() var applicationAggregate = _appMetadata.CanonicalApplicationName.ApplicationComponent; // Assert - applicationAggregate.Should().Be("ExpectedAppComponent"); + applicationAggregate.Should().Be("expected_app_component"); } [Fact] @@ -106,9 +108,9 @@ public void StartTimestamp_should_stay_constant_over_instances() var appMetadata1 = new ApplicationMetadata( entryAssembly!, - "DoesNotMatter1", + "does_not_matter_1", "dnm1", - "DoesNotMatter1", + "does_not_matter_1", "dnm1", entryAssemblyName!, "UnitTestEnvironment"); @@ -116,9 +118,9 @@ public void StartTimestamp_should_stay_constant_over_instances() var appMetadata2 = new ApplicationMetadata( entryAssembly!, - "DoesNotMatter2", + "does_not_matter_2", "dnm2", - "DoesNotMatter2", + "does_not_matter_2", "dnm2", entryAssemblyName!, "UnitTestEnvironment"); @@ -173,4 +175,30 @@ public void Auto_Assembly_detection_detects_something() _appMetadataWithJustAggregateAndEnvironment.ApplicationName.Should() .NotBeNullOrWhiteSpace(); } + + [Fact] + public void GetApplicationMetadataFromEntryAssemblyAndConfiguration_should_flow_values_through() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["netchris:application:aggregate"] = "expected_app_aggregate", + ["netchris:application:aggregateShort"] = "eaa", + ["netchris:application:component"] = "expected_app_component", + ["netchris:application:componentShort"] = "eac", + }) + .Build(); + + // Act + var appMetadata = ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration( + configuration, "UnitTestEnvironment"); + + // Assert + appMetadata.CanonicalApplicationName.ApplicationAggregate.Should().Be("expected_app_aggregate"); + appMetadata.CanonicalApplicationName.ApplicationAggregateShort.Should().Be("eaa"); + appMetadata.CanonicalApplicationName.ApplicationComponent.Should().Be("expected_app_component"); + appMetadata.CanonicalApplicationName.ApplicationComponentShort.Should().Be("eac"); + appMetadata.EnvironmentName.Should().Be("UnitTestEnvironment"); + } } \ No newline at end of file diff --git a/NetChris.Core.UnitTests/CanonicalApplicationNameTests.cs b/NetChris.Core.UnitTests/CanonicalApplicationNameTests.cs new file mode 100644 index 0000000..b86b2e4 --- /dev/null +++ b/NetChris.Core.UnitTests/CanonicalApplicationNameTests.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace NetChris.Core.UnitTests; + +public class CanonicalApplicationNameTests +{ + [Fact] + public void Conforming_values_should_flow_through() + { + // Arrange + // Act + var canonicalApplicationName = new CanonicalApplicationName( + "app_aggregate", "aagg", "app_component", "acmp"); + + // Assert + canonicalApplicationName.ApplicationAggregate.Should().Be("app_aggregate"); + canonicalApplicationName.ApplicationAggregateShort.Should().Be("aagg"); + canonicalApplicationName.ApplicationComponent.Should().Be("app_component"); + canonicalApplicationName.ApplicationComponentShort.Should().Be("acmp"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Aggregate")] + [InlineData("1aggregate")] + [InlineData("app-aggregate")] + [InlineData("app aggregate")] + public void Non_conforming_long_form_aggregate_should_throw(string? applicationAggregate) + { + // Arrange + // Act + var act = () => new CanonicalApplicationName(applicationAggregate!, "aagg", "app_component", "acmp"); + + // Assert + act.Should().Throw() + .WithParameterName("applicationAggregate"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Component")] + [InlineData("1component")] + [InlineData("app-component")] + public void Non_conforming_long_form_component_should_throw(string? applicationComponent) + { + // Arrange + // Act + var act = () => new CanonicalApplicationName("app_aggregate", "aagg", applicationComponent!, "acmp"); + + // Assert + act.Should().Throw() + .WithParameterName("applicationComponent"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("AAGG")] + [InlineData("aagg5")] + [InlineData("ag_g")] + public void Non_conforming_short_form_aggregate_should_throw(string? applicationAggregateShort) + { + // Arrange + // Act + var act = () => + new CanonicalApplicationName("app_aggregate", applicationAggregateShort!, "app_component", "acmp"); + + // Assert + act.Should().Throw() + .WithParameterName("applicationAggregateShort"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("ACMP")] + [InlineData("acmp5")] + [InlineData("cm_p")] + public void Non_conforming_short_form_component_should_throw(string? applicationComponentShort) + { + // Arrange + // Act + var act = () => + new CanonicalApplicationName("app_aggregate", "aagg", "app_component", applicationComponentShort!); + + // Assert + act.Should().Throw() + .WithParameterName("applicationComponentShort"); + } + + [Fact] + public void FromConfiguration_should_build_from_well_known_keys() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["netchris:application:aggregate"] = "app_aggregate", + ["netchris:application:aggregateShort"] = "aagg", + ["netchris:application:component"] = "app_component", + ["netchris:application:componentShort"] = "acmp", + }) + .Build(); + + // Act + var canonicalApplicationName = CanonicalApplicationName.FromConfiguration(configuration); + + // Assert + canonicalApplicationName.ApplicationAggregate.Should().Be("app_aggregate"); + canonicalApplicationName.ApplicationAggregateShort.Should().Be("aagg"); + canonicalApplicationName.ApplicationComponent.Should().Be("app_component"); + canonicalApplicationName.ApplicationComponentShort.Should().Be("acmp"); + } + + [Fact] + public void FromConfiguration_with_null_configuration_should_throw() + { + // Arrange + // Act + var act = () => CanonicalApplicationName.FromConfiguration(null!); + + // Assert + act.Should().Throw() + .WithParameterName("configuration"); + } + + [Theory] + [InlineData("netchris:application:aggregate")] + [InlineData("netchris:application:aggregateShort")] + [InlineData("netchris:application:component")] + [InlineData("netchris:application:componentShort")] + public void FromConfiguration_with_missing_key_should_throw_and_name_the_key(string missingKey) + { + // Arrange + var values = new Dictionary + { + ["netchris:application:aggregate"] = "app_aggregate", + ["netchris:application:aggregateShort"] = "aagg", + ["netchris:application:component"] = "app_component", + ["netchris:application:componentShort"] = "acmp", + }; + values.Remove(missingKey); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + // Act + var act = () => CanonicalApplicationName.FromConfiguration(configuration); + + // Assert + act.Should().Throw() + .WithMessage($"*{missingKey}*"); + } +} diff --git a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj index e6ca6f7..a05b6ac 100644 --- a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj +++ b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj @@ -19,6 +19,7 @@ + diff --git a/NetChris.Core/ApplicationMetadata.cs b/NetChris.Core/ApplicationMetadata.cs index 7f75564..9f1028d 100644 --- a/NetChris.Core/ApplicationMetadata.cs +++ b/NetChris.Core/ApplicationMetadata.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Microsoft.Extensions.Configuration; namespace NetChris.Core; @@ -48,6 +49,29 @@ public static ApplicationMetadata GetApplicationMetadataFromEntryAssembly( return result; } + /// + /// Gets a new instance of the class, using + /// for the assembly on which to base its data, and + /// to determine its . + /// + /// The configuration from which to read the Canonical Application Name. + /// The environment in which the application is running. + /// In this factory, is automatically discerned + /// from using its . + public static ApplicationMetadata GetApplicationMetadataFromEntryAssemblyAndConfiguration( + IConfiguration configuration, + string environmentName) + { + var canonicalApplicationName = CanonicalApplicationName.FromConfiguration(configuration); + + return GetApplicationMetadataFromEntryAssembly( + canonicalApplicationName.ApplicationAggregate, + canonicalApplicationName.ApplicationAggregateShort, + canonicalApplicationName.ApplicationComponent, + canonicalApplicationName.ApplicationComponentShort, + environmentName); + } + /// /// Initializes a new instance of the class. /// diff --git a/NetChris.Core/CanonicalApplicationName.cs b/NetChris.Core/CanonicalApplicationName.cs index fd4e269..1f0c43f 100644 --- a/NetChris.Core/CanonicalApplicationName.cs +++ b/NetChris.Core/CanonicalApplicationName.cs @@ -1,13 +1,20 @@ using System; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Configuration; namespace NetChris.Core; /// /// The application's Canonical Application Name /// -/// Canonical Application Name +/// Canonical Application Name public class CanonicalApplicationName { + private static readonly Regex LongFormPattern = new("^[a-z][a-z0-9_]*$", RegexOptions.Compiled); + private static readonly Regex ShortFormPattern = new("^[a-z0-9]{1,4}$", RegexOptions.Compiled); + + private const string NetChrisApplicationConfigurationSectionKey = "netchris:application"; + /// /// Create a new /// @@ -15,18 +22,14 @@ public class CanonicalApplicationName /// The short-form application aggregate /// The application component /// The short-form application component + /// Name part normalization public CanonicalApplicationName(string applicationAggregate, string applicationAggregateShort, string applicationComponent, string applicationComponentShort) { - if (string.IsNullOrWhiteSpace(applicationAggregate)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(applicationAggregate)); - } - - if (string.IsNullOrWhiteSpace(applicationComponent)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(applicationComponent)); - } + ValidateLongForm(applicationAggregate, nameof(applicationAggregate)); + ValidateShortForm(applicationAggregateShort, nameof(applicationAggregateShort)); + ValidateLongForm(applicationComponent, nameof(applicationComponent)); + ValidateShortForm(applicationComponentShort, nameof(applicationComponentShort)); ApplicationAggregate = applicationAggregate; ApplicationAggregateShort = applicationAggregateShort; @@ -34,6 +37,26 @@ public CanonicalApplicationName(string applicationAggregate, string applicationA ApplicationComponentShort = applicationComponentShort; } + private static void ValidateLongForm(string value, string paramName) + { + if (string.IsNullOrWhiteSpace(value) || !LongFormPattern.IsMatch(value)) + { + throw new ArgumentException( + "Value must consist of lower-case letters, numbers, and underscores only, and must begin with a lower-case letter.", + paramName); + } + } + + private static void ValidateShortForm(string value, string paramName) + { + if (string.IsNullOrWhiteSpace(value) || !ShortFormPattern.IsMatch(value)) + { + throw new ArgumentException( + "Value must be 1 to 4 characters long and consist of lower-case letters and numbers only.", + paramName); + } + } + /// /// Gets the application aggregate /// @@ -53,4 +76,42 @@ public CanonicalApplicationName(string applicationAggregate, string applicationA /// Gets the short-form application component /// public string ApplicationComponentShort { get; } + + /// + /// Creates a new from well-known keys in : + /// netchris:application:aggregate, netchris:application:aggregateShort, + /// netchris:application:component, and netchris:application:componentShort. + /// + /// The configuration from which to read the Canonical Application Name. + /// Thrown if is . + /// Thrown if any of the required configuration keys are missing or empty. + public static CanonicalApplicationName FromConfiguration(IConfiguration configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var applicationAggregate = GetRequiredValue(configuration, "aggregate"); + var applicationAggregateShort = GetRequiredValue(configuration, "aggregateShort"); + var applicationComponent = GetRequiredValue(configuration, "component"); + var applicationComponentShort = GetRequiredValue(configuration, "componentShort"); + + return new CanonicalApplicationName(applicationAggregate, applicationAggregateShort, + applicationComponent, applicationComponentShort); + } + + private static string GetRequiredValue(IConfiguration configuration, string keyName) + { + var fullKey = $"{NetChrisApplicationConfigurationSectionKey}:{keyName}"; + var value = configuration[fullKey]; + + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException( + $"The configuration key '{fullKey}' is required to create a {nameof(CanonicalApplicationName)} but was missing or empty."); + } + + return value!; + } } \ No newline at end of file diff --git a/NetChris.Core/NetChris.Core.csproj b/NetChris.Core/NetChris.Core.csproj index fd6adb9..3c13397 100644 --- a/NetChris.Core/NetChris.Core.csproj +++ b/NetChris.Core/NetChris.Core.csproj @@ -25,6 +25,7 @@ + From a56c56f79f84ec52ac321d4ef6a99a362932302f Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:29:29 -0700 Subject: [PATCH 03/10] Move agent directives into AGENTS.md, have CLAUDE.md point to it Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 1 + CLAUDE.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ced8182 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +At the start of every session, read the resource `chris-simmons://public-mcp/agent/rules/dotnet` from MCP server `plugin:public-mcp-server:public-mcp` and apply the rules. diff --git a/CLAUDE.md b/CLAUDE.md index ced8182..10ddb19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -At the start of every session, read the resource `chris-simmons://public-mcp/agent/rules/dotnet` from MCP server `plugin:public-mcp-server:public-mcp` and apply the rules. +See @AGENTS.md From d786659ccb417610b0c7ac75854c4039967bbf68 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:44:20 -0700 Subject: [PATCH 04/10] Document CanonicalApplicationName and its configuration keys in Package-README Co-Authored-By: Claude Sonnet 5 --- NetChris.Core/Package-README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/NetChris.Core/Package-README.md b/NetChris.Core/Package-README.md index db572a4..133b0a5 100644 --- a/NetChris.Core/Package-README.md +++ b/NetChris.Core/Package-README.md @@ -16,3 +16,19 @@ You can find other examples of this pattern in something like [`Ardalis.Result`] ### CommandResult References - [Working with the result pattern](https://andrewlock.net/series/working-with-the-result-pattern/) + +## CanonicalApplicationName + +Represents the application's [Canonical Application Name](https://github.com/NetChris/reference/wiki/Canonical-Application-Name): the application aggregate and application component, each with a long and short form. Values must adhere to the [name part normalization](https://github.com/NetChris/reference/wiki/Name-part-normalization) rules: + +- Long form (`applicationAggregate`, `applicationComponent`): lower-case letters, numbers, and underscores only, beginning with a lower-case letter. +- Short form (`applicationAggregateShort`, `applicationComponentShort`): 1 to 4 lower-case letters and/or numbers only. + +`CanonicalApplicationName.FromConfiguration(IConfiguration)` builds an instance from the following well-known configuration keys, failing fast with a clear error if any is missing or empty: + +- `netchris:application:aggregate` +- `netchris:application:aggregateShort` +- `netchris:application:component` +- `netchris:application:componentShort` + +`ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration(IConfiguration, environmentName)` uses the same configuration keys to build a full `ApplicationMetadata` instance from the entry assembly. From b67d53eb0ea418a981cb04a74926ac532e945df7 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:47:44 -0700 Subject: [PATCH 05/10] Give ApplicationMetadata its own README section Co-Authored-By: Claude Sonnet 5 --- NetChris.Core/Package-README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NetChris.Core/Package-README.md b/NetChris.Core/Package-README.md index 133b0a5..27c141f 100644 --- a/NetChris.Core/Package-README.md +++ b/NetChris.Core/Package-README.md @@ -31,4 +31,9 @@ Represents the application's [Canonical Application Name](https://github.com/Net - `netchris:application:component` - `netchris:application:componentShort` -`ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration(IConfiguration, environmentName)` uses the same configuration keys to build a full `ApplicationMetadata` instance from the entry assembly. +## ApplicationMetadata + +`IApplicationMetadata`/`ApplicationMetadata` capture basic metadata about the running application and its execution instance: `ApplicationName`, `CanonicalApplicationName`, `ApplicationVersion`, `InformationalVersion`, `EnvironmentName`, `MachineName`, `OSPlatform`, `OSVersion`, `UserName`, `ClrVersion`, and `StartTimestamp`. + +- `ApplicationMetadata.GetApplicationMetadataFromEntryAssembly(applicationAggregate, applicationAggregateShort, applicationComponent, applicationComponentShort, environmentName)` builds an instance from `Assembly.GetEntryAssembly()`, discerning `ApplicationName` from the entry assembly's name. +- `ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration(IConfiguration configuration, environmentName)` does the same, but builds its `CanonicalApplicationName` via `CanonicalApplicationName.FromConfiguration(configuration)` using the well-known configuration keys above. From fca0670060dbe7c065403635d7f4a18098128e1e Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:51:17 -0700 Subject: [PATCH 06/10] Upgrade unit test packages --- NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj index a05b6ac..99e1c23 100644 --- a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj +++ b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj @@ -18,15 +18,15 @@ - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 883f16b47e1a9be5a4a5197797eb21449bcf0aff Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:52:23 -0700 Subject: [PATCH 07/10] Upgrade test project to xunit.v3 Co-Authored-By: Claude Sonnet 5 --- NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj index 99e1c23..af8c383 100644 --- a/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj +++ b/NetChris.Core.UnitTests/NetChris.Core.UnitTests.csproj @@ -21,7 +21,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 1c967e6d16581fd0decbc7c16ccd340ec9a06bd4 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 15:55:03 -0700 Subject: [PATCH 08/10] Upgrade packages --- NetChris.Core/NetChris.Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetChris.Core/NetChris.Core.csproj b/NetChris.Core/NetChris.Core.csproj index 3c13397..f93be89 100644 --- a/NetChris.Core/NetChris.Core.csproj +++ b/NetChris.Core/NetChris.Core.csproj @@ -24,8 +24,8 @@ - - + + From bc9009aa23c1a3b27c15934c17eadf361d126cb0 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 16:07:28 -0700 Subject: [PATCH 09/10] Rename ApplicationMetadata factories to take an explicit Assembly (breaking change) GetApplicationMetadataFromEntryAssembly -> GetApplicationMetadata(Assembly, ...) GetApplicationMetadataFromEntryAssemblyAndConfiguration -> GetApplicationMetadataFromConfiguration(Assembly, IConfiguration, ...) Callers now supply the assembly explicitly instead of relying on Assembly.GetEntryAssembly(), which is unreliable for test hosts and other non-standard entry points. Co-Authored-By: Claude Sonnet 5 --- .../ApplicationMetadataTests.cs | 18 +++++++----- NetChris.Core/ApplicationMetadata.cs | 29 ++++++++++--------- NetChris.Core/Package-README.md | 4 +-- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs index 783d8d2..3909b6d 100644 --- a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs +++ b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs @@ -10,7 +10,7 @@ namespace NetChris.Core.UnitTests; public class ApplicationMetadataTests { private readonly ApplicationMetadata _appMetadata; - private readonly ApplicationMetadata _appMetadataWithJustAggregateAndEnvironment; + private readonly ApplicationMetadata _appMetadataFromFactory; public ApplicationMetadataTests() { @@ -26,8 +26,9 @@ public ApplicationMetadataTests() thisAssembly.GetName().Name!, "UnitTestEnvironment"); - _appMetadataWithJustAggregateAndEnvironment = - ApplicationMetadata.GetApplicationMetadataFromEntryAssembly( + _appMetadataFromFactory = + ApplicationMetadata.GetApplicationMetadata( + thisAssembly, "expected_app_aggregate", "eaa", "expected_app_component", @@ -170,16 +171,17 @@ public void ClrVersion_should_have_a_value() } [Fact] - public void Auto_Assembly_detection_detects_something() + public void GetApplicationMetadata_discerns_ApplicationName_from_assembly() { - _appMetadataWithJustAggregateAndEnvironment.ApplicationName.Should() + _appMetadataFromFactory.ApplicationName.Should() .NotBeNullOrWhiteSpace(); } [Fact] - public void GetApplicationMetadataFromEntryAssemblyAndConfiguration_should_flow_values_through() + public void GetApplicationMetadataFromConfiguration_should_flow_values_through() { // Arrange + var thisAssembly = typeof(ApplicationMetadataTests).Assembly; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { @@ -191,8 +193,8 @@ public void GetApplicationMetadataFromEntryAssemblyAndConfiguration_should_flow_ .Build(); // Act - var appMetadata = ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration( - configuration, "UnitTestEnvironment"); + var appMetadata = ApplicationMetadata.GetApplicationMetadataFromConfiguration( + thisAssembly, configuration, "UnitTestEnvironment"); // Assert appMetadata.CanonicalApplicationName.ApplicationAggregate.Should().Be("expected_app_aggregate"); diff --git a/NetChris.Core/ApplicationMetadata.cs b/NetChris.Core/ApplicationMetadata.cs index 9f1028d..ba033e3 100644 --- a/NetChris.Core/ApplicationMetadata.cs +++ b/NetChris.Core/ApplicationMetadata.cs @@ -11,38 +11,37 @@ namespace NetChris.Core; public class ApplicationMetadata : IApplicationMetadata { /// - /// Gets a new instance of the class, using - /// for the assembly on which to base its data. + /// Gets a new instance of the class. /// + /// The assembly from which to pull the /// The application aggregate /// The short-form application aggregate /// The application component /// The short-form application component /// The environment in which the application is running. /// In this factory, is automatically discerned - /// from using its . - public static ApplicationMetadata GetApplicationMetadataFromEntryAssembly( + /// from using its . + public static ApplicationMetadata GetApplicationMetadata( + Assembly assembly, string applicationAggregate, string applicationAggregateShort, string applicationComponent, string applicationComponentShort, string environmentName) { - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) + if (assembly is null) { - throw new InvalidOperationException( - "There was no assembly available from System.Reflection.Assembly.GetEntryAssembly()"); + throw new ArgumentNullException(nameof(assembly)); } - var assemblySimpleName = entryAssembly.GetName().Name; + var assemblySimpleName = assembly.GetName().Name; if (assemblySimpleName == null) { throw new InvalidOperationException("The assembly's simple name was null"); } - var result = new ApplicationMetadata(entryAssembly, + var result = new ApplicationMetadata(assembly, applicationAggregate, applicationAggregateShort, applicationComponent, applicationComponentShort, assemblySimpleName, environmentName); @@ -51,20 +50,22 @@ public static ApplicationMetadata GetApplicationMetadataFromEntryAssembly( /// /// Gets a new instance of the class, using - /// for the assembly on which to base its data, and /// to determine its . /// + /// The assembly from which to pull the /// The configuration from which to read the Canonical Application Name. /// The environment in which the application is running. /// In this factory, is automatically discerned - /// from using its . - public static ApplicationMetadata GetApplicationMetadataFromEntryAssemblyAndConfiguration( + /// from using its . + public static ApplicationMetadata GetApplicationMetadataFromConfiguration( + Assembly assembly, IConfiguration configuration, string environmentName) { var canonicalApplicationName = CanonicalApplicationName.FromConfiguration(configuration); - return GetApplicationMetadataFromEntryAssembly( + return GetApplicationMetadata( + assembly, canonicalApplicationName.ApplicationAggregate, canonicalApplicationName.ApplicationAggregateShort, canonicalApplicationName.ApplicationComponent, diff --git a/NetChris.Core/Package-README.md b/NetChris.Core/Package-README.md index 27c141f..286aebb 100644 --- a/NetChris.Core/Package-README.md +++ b/NetChris.Core/Package-README.md @@ -35,5 +35,5 @@ Represents the application's [Canonical Application Name](https://github.com/Net `IApplicationMetadata`/`ApplicationMetadata` capture basic metadata about the running application and its execution instance: `ApplicationName`, `CanonicalApplicationName`, `ApplicationVersion`, `InformationalVersion`, `EnvironmentName`, `MachineName`, `OSPlatform`, `OSVersion`, `UserName`, `ClrVersion`, and `StartTimestamp`. -- `ApplicationMetadata.GetApplicationMetadataFromEntryAssembly(applicationAggregate, applicationAggregateShort, applicationComponent, applicationComponentShort, environmentName)` builds an instance from `Assembly.GetEntryAssembly()`, discerning `ApplicationName` from the entry assembly's name. -- `ApplicationMetadata.GetApplicationMetadataFromEntryAssemblyAndConfiguration(IConfiguration configuration, environmentName)` does the same, but builds its `CanonicalApplicationName` via `CanonicalApplicationName.FromConfiguration(configuration)` using the well-known configuration keys above. +- `ApplicationMetadata.GetApplicationMetadata(assembly, applicationAggregate, applicationAggregateShort, applicationComponent, applicationComponentShort, environmentName)` builds an instance, discerning `ApplicationName` from the given assembly's name. +- `ApplicationMetadata.GetApplicationMetadataFromConfiguration(assembly, configuration, environmentName)` does the same, but builds its `CanonicalApplicationName` via `CanonicalApplicationName.FromConfiguration(configuration)` using the well-known configuration keys above. From 7e8f164ade9d3b2c6ded8c288bcc236faecf9789 Mon Sep 17 00:00:00 2001 From: Chris Simmons Date: Sat, 11 Jul 2026 16:15:57 -0700 Subject: [PATCH 10/10] Make GetApplicationMetadataFromConfiguration an overload of GetApplicationMetadata Co-Authored-By: Claude Sonnet 5 --- NetChris.Core.UnitTests/ApplicationMetadataTests.cs | 4 ++-- NetChris.Core/ApplicationMetadata.cs | 2 +- NetChris.Core/Package-README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs index 3909b6d..94a7185 100644 --- a/NetChris.Core.UnitTests/ApplicationMetadataTests.cs +++ b/NetChris.Core.UnitTests/ApplicationMetadataTests.cs @@ -178,7 +178,7 @@ public void GetApplicationMetadata_discerns_ApplicationName_from_assembly() } [Fact] - public void GetApplicationMetadataFromConfiguration_should_flow_values_through() + public void GetApplicationMetadata_from_configuration_should_flow_values_through() { // Arrange var thisAssembly = typeof(ApplicationMetadataTests).Assembly; @@ -193,7 +193,7 @@ public void GetApplicationMetadataFromConfiguration_should_flow_values_through() .Build(); // Act - var appMetadata = ApplicationMetadata.GetApplicationMetadataFromConfiguration( + var appMetadata = ApplicationMetadata.GetApplicationMetadata( thisAssembly, configuration, "UnitTestEnvironment"); // Assert diff --git a/NetChris.Core/ApplicationMetadata.cs b/NetChris.Core/ApplicationMetadata.cs index ba033e3..bc89446 100644 --- a/NetChris.Core/ApplicationMetadata.cs +++ b/NetChris.Core/ApplicationMetadata.cs @@ -57,7 +57,7 @@ public static ApplicationMetadata GetApplicationMetadata( /// The environment in which the application is running. /// In this factory, is automatically discerned /// from using its . - public static ApplicationMetadata GetApplicationMetadataFromConfiguration( + public static ApplicationMetadata GetApplicationMetadata( Assembly assembly, IConfiguration configuration, string environmentName) diff --git a/NetChris.Core/Package-README.md b/NetChris.Core/Package-README.md index 286aebb..be23d9b 100644 --- a/NetChris.Core/Package-README.md +++ b/NetChris.Core/Package-README.md @@ -36,4 +36,4 @@ Represents the application's [Canonical Application Name](https://github.com/Net `IApplicationMetadata`/`ApplicationMetadata` capture basic metadata about the running application and its execution instance: `ApplicationName`, `CanonicalApplicationName`, `ApplicationVersion`, `InformationalVersion`, `EnvironmentName`, `MachineName`, `OSPlatform`, `OSVersion`, `UserName`, `ClrVersion`, and `StartTimestamp`. - `ApplicationMetadata.GetApplicationMetadata(assembly, applicationAggregate, applicationAggregateShort, applicationComponent, applicationComponentShort, environmentName)` builds an instance, discerning `ApplicationName` from the given assembly's name. -- `ApplicationMetadata.GetApplicationMetadataFromConfiguration(assembly, configuration, environmentName)` does the same, but builds its `CanonicalApplicationName` via `CanonicalApplicationName.FromConfiguration(configuration)` using the well-known configuration keys above. +- `ApplicationMetadata.GetApplicationMetadata(assembly, configuration, environmentName)` does the same, but builds its `CanonicalApplicationName` via `CanonicalApplicationName.FromConfiguration(configuration)` using the well-known configuration keys above.