Skip to content

Commit 9cd9c6d

Browse files
authored
Allow configuring global default quantities (#1692)
Extracted from #1544 because choosing the global quantity catalog is an independent setup capability, not a requirement of fractional QuantityValue. Keeping it separate makes the singleton initialization and compatibility implications explicit and allows this commit to be deferred without blocking the isolated factory APIs. Changes: - Lazily create UnitsNetSetup.Default from the existing setup builder. - Add ConfigureDefaults for selecting the global catalog before first use. - Synchronize configuration and creation so concurrent first access cannot observe a partially configured builder. - Reject configuration after the singleton has been created. Tests: - Add a dedicated test assembly so global singleton state is isolated from the existing suite. - Verify configured catalog selection, default cache/parser wiring, excluded units, and rejection of reconfiguration.
1 parent 670ba36 commit 9cd9c6d

8 files changed

Lines changed: 177 additions & 8 deletions

File tree

.github/workflows/net48-compatibility.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252
run: |
5353
$testProjects = @(
5454
'UnitsNet.Tests/UnitsNet.Tests.csproj',
55+
'UnitsNet.GlobalSetup.DefaultFirst.Tests/UnitsNet.GlobalSetup.DefaultFirst.Tests.csproj',
56+
'UnitsNet.GlobalSetup.Tests/UnitsNet.GlobalSetup.Tests.csproj',
5557
'UnitsNet.NumberExtensions.Tests/UnitsNet.NumberExtensions.Tests.csproj',
5658
'UnitsNet.NumberExtensions.CS14.Tests/UnitsNet.NumberExtensions.CS14.Tests.csproj',
5759
'UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj'

Build/build-functions.psm1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ function Start-Tests {
4040

4141
$projectPaths = @(
4242
"UnitsNet.Tests/UnitsNet.Tests.csproj",
43+
"UnitsNet.GlobalSetup.DefaultFirst.Tests/UnitsNet.GlobalSetup.DefaultFirst.Tests.csproj",
44+
"UnitsNet.GlobalSetup.Tests/UnitsNet.GlobalSetup.Tests.csproj",
4345
"UnitsNet.NumberExtensions.Tests/UnitsNet.NumberExtensions.Tests.csproj",
4446
"UnitsNet.NumberExtensions.CS14.Tests/UnitsNet.NumberExtensions.CS14.Tests.csproj",
4547
"UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net10.0</TargetFrameworks>
5+
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net48</TargetFrameworks>
6+
<LangVersion>latest</LangVersion>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<IsTestProject>true</IsTestProject>
9+
<Nullable>enable</Nullable>
10+
<TestFramework>xunit</TestFramework>
11+
</PropertyGroup>
12+
13+
<PropertyGroup>
14+
<AssemblyOriginatorKeyFile>../UnitsNet.snk</AssemblyOriginatorKeyFile>
15+
<DelaySign>false</DelaySign>
16+
<SignAssembly>true</SignAssembly>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
21+
<PackageReference Include="xunit" />
22+
<PackageReference Include="xunit.runner.visualstudio">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<ProjectReference Include="../UnitsNet/UnitsNet.csproj" />
30+
</ItemGroup>
31+
32+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using Xunit;
5+
6+
namespace UnitsNet.GlobalSetup.DefaultFirst.Tests;
7+
8+
/// <summary>
9+
/// Tests configuring the process-wide default setup after first use.
10+
/// </summary>
11+
/// <remarks>
12+
/// This assembly intentionally contains a single test because <see cref="UnitsNetSetup.Default" /> cannot be reset.
13+
/// </remarks>
14+
public class UnitsNetSetupDefaultFirstTests
15+
{
16+
[Fact]
17+
public void ConfigureDefaults_AfterDefaultIsCreated_ThrowsInvalidOperationException()
18+
{
19+
UnitsNetSetup originalDefault = UnitsNetSetup.Default;
20+
bool configurationInvoked = false;
21+
22+
Assert.Throws<InvalidOperationException>(() => UnitsNetSetup.ConfigureDefaults(_ => configurationInvoked = true));
23+
24+
Assert.False(configurationInvoked);
25+
Assert.Same(originalDefault, UnitsNetSetup.Default);
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net10.0</TargetFrameworks>
5+
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net48</TargetFrameworks>
6+
<LangVersion>latest</LangVersion>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<IsTestProject>true</IsTestProject>
9+
<Nullable>enable</Nullable>
10+
<TestFramework>xunit</TestFramework>
11+
</PropertyGroup>
12+
13+
<PropertyGroup>
14+
<AssemblyOriginatorKeyFile>../UnitsNet.snk</AssemblyOriginatorKeyFile>
15+
<DelaySign>false</DelaySign>
16+
<SignAssembly>true</SignAssembly>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
21+
<PackageReference Include="xunit" />
22+
<PackageReference Include="xunit.runner.visualstudio">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<ProjectReference Include="../UnitsNet/UnitsNet.csproj" />
30+
</ItemGroup>
31+
32+
</Project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using UnitsNet.Units;
5+
using Xunit;
6+
7+
namespace UnitsNet.GlobalSetup.Tests;
8+
9+
/// <summary>
10+
/// Tests configuring the process-wide default setup before first use.
11+
/// </summary>
12+
/// <remarks>
13+
/// This assembly intentionally contains a single test because <see cref="UnitsNetSetup.Default" /> cannot be reset.
14+
/// Keep additional global-configuration scenarios isolated in separate test processes.
15+
/// </remarks>
16+
public class UnitsNetSetupGlobalConfigurationTests
17+
{
18+
[Fact]
19+
public void ConfigureDefaults_BeforeFirstUse_ConfiguresAndFreezesDefaultSetup()
20+
{
21+
Assert.Equal("configuration",
22+
Assert.Throws<ArgumentNullException>(() => UnitsNetSetup.ConfigureDefaults(null!)).ParamName);
23+
24+
UnitsNetSetup configured = UnitsNetSetup.ConfigureDefaults(builder => builder.WithQuantities([Mass.Info]));
25+
26+
Assert.Same(configured, UnitsNetSetup.Default);
27+
Assert.Same(configured.UnitAbbreviations, UnitAbbreviationsCache.Default);
28+
Assert.Same(configured.UnitParser, UnitParser.Default);
29+
Assert.Equal([Mass.Info], configured.Quantities.Infos);
30+
Assert.Throws<UnitNotFoundException>(() => configured.UnitParser.Parse<LengthUnit>("m"));
31+
Assert.Throws<InvalidOperationException>(() => UnitsNetSetup.ConfigureDefaults(_ => { }));
32+
}
33+
}

UnitsNet.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
</Folder>
4444
<Project Path="CodeGen/CodeGen.csproj" />
4545
<Project Path="UnitsNet.Benchmark/UnitsNet.Benchmark.csproj" />
46+
<Project Path="UnitsNet.GlobalSetup.DefaultFirst.Tests/UnitsNet.GlobalSetup.DefaultFirst.Tests.csproj" />
47+
<Project Path="UnitsNet.GlobalSetup.Tests/UnitsNet.GlobalSetup.Tests.csproj" />
4648
<Project Path="UnitsNet.NumberExtensions.CS14.Tests\UnitsNet.NumberExtensions.CS14.Tests.csproj" Type="Classic C#" />
4749
<Project Path="UnitsNet.NumberExtensions.CS14\UnitsNet.NumberExtensions.CS14.csproj" Type="Classic C#" />
4850
<Project Path="UnitsNet.NumberExtensions.Tests/UnitsNet.NumberExtensions.Tests.csproj" />

UnitsNet/CustomCode/UnitsNetSetup.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ namespace UnitsNet;
1414
/// </summary>
1515
public sealed class UnitsNetSetup
1616
{
17+
/// <summary>
18+
/// Synchronizes the default builder swap and creation checks with the value creation already synchronized by <see cref="Lazy{T}" />.
19+
/// </summary>
20+
private static readonly object DefaultConfigurationLock = new();
21+
private static DefaultConfigurationBuilder _defaultConfigurationBuilder = new();
22+
private static readonly Lazy<UnitsNetSetup> DefaultConfiguration = new(BuildDefault);
23+
1724
/// <summary>
1825
/// Builds a UnitsNet setup by selecting built-in or external quantity definitions.
1926
/// </summary>
@@ -92,14 +99,12 @@ internal UnitsNetSetup Build()
9299
}
93100
}
94101

95-
static UnitsNetSetup()
102+
private static UnitsNetSetup BuildDefault()
96103
{
97-
IReadOnlyCollection<QuantityInfo> quantityInfos = Quantity.DefaultProvider.Quantities;
98-
99-
// note: in order to support the ConvertByAbbreviation, the unit converter should require a UnitParser in the constructor
100-
var unitConverter = UnitConverter.CreateDefault();
101-
102-
Default = new UnitsNetSetup(quantityInfos, unitConverter);
104+
lock (DefaultConfigurationLock)
105+
{
106+
return _defaultConfigurationBuilder.Build();
107+
}
103108
}
104109

105110
/// <summary>
@@ -116,6 +121,37 @@ public static UnitsNetSetup Create(Action<DefaultConfigurationBuilder> configura
116121
return builder.Build();
117122
}
118123

124+
/// <summary>
125+
/// Configures and creates the global default setup before its first use.
126+
/// </summary>
127+
/// <param name="configuration">Configures the quantities included in the default setup.</param>
128+
/// <returns>The configured global default setup.</returns>
129+
/// <exception cref="InvalidOperationException">The default setup has already been created.</exception>
130+
/// <seealso cref="Default" />
131+
public static UnitsNetSetup ConfigureDefaults(Action<DefaultConfigurationBuilder> configuration)
132+
{
133+
if (configuration is null) throw new ArgumentNullException(nameof(configuration));
134+
135+
lock (DefaultConfigurationLock)
136+
{
137+
if (DefaultConfiguration.IsValueCreated)
138+
{
139+
throw new InvalidOperationException("The default configuration cannot be changed after it has been created.");
140+
}
141+
142+
var builder = new DefaultConfigurationBuilder();
143+
configuration(builder);
144+
145+
if (DefaultConfiguration.IsValueCreated)
146+
{
147+
throw new InvalidOperationException("The default configuration was created while it was being configured.");
148+
}
149+
150+
_defaultConfigurationBuilder = builder;
151+
return DefaultConfiguration.Value;
152+
}
153+
}
154+
119155
/// <summary>
120156
/// Create a new UnitsNet setup with the given quantities, their units and unit conversion functions between units.
121157
/// </summary>
@@ -140,11 +176,14 @@ public UnitsNetSetup(IEnumerable<QuantityInfo> quantityInfos, UnitConverter unit
140176
/// provided.
141177
/// </summary>
142178
/// <remarks>
179+
/// Call <see cref="ConfigureDefaults" /> before first accessing this property to select a different quantity catalog.<br />
180+
/// <br />
143181
/// Manipulating this instance, such as adding new units or changing default unit abbreviations, will affect most
144182
/// usages of UnitsNet in the
145183
/// current AppDomain since the typical use is via static members and not providing a setup instance.
146184
/// </remarks>
147-
public static UnitsNetSetup Default { get; }
185+
/// <seealso cref="ConfigureDefaults" />
186+
public static UnitsNetSetup Default => DefaultConfiguration.Value;
148187

149188
/// <summary>
150189
/// Converts between units of a quantity, such as from meters to centimeters of a given length.

0 commit comments

Comments
 (0)