From aca82febc8f32cdf8dab8e97313b196fa8338f4f Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 31 Jan 2025 02:34:32 +0200
Subject: [PATCH 1/6] Enum boxing optimizations: - introduce the UnitKey struct
 as a replacement for the conversions from TUnit to Enum - re-map the
 UnitAbbreviationCache using the UnitKey for the concurrent dictionary -
 remove all Convert.ToInt32(..) calls - optimize the QuantityInfoLookup
 collections - refactored some of the Quantity/UnitConverter code (using the
 UnitParser/UnitAbbreviationsCache) - added some tests and benchmarks covering
 the new modifications

---
 Directory.Packages.props                      |   1 +
 .../QuantityFromStringBenchmarks.cs           | 100 ++++++
 .../QuantityFromUnitAbbreviationBenchmarks.cs | 101 ++++++
 .../QuantityFromUnitNameBenchmarks.cs         |  90 +++++
 .../ToStringWithDefaultPrecisionBenchmarks.cs |  56 +++
 .../ToValue/ConvertValueBenchmarks.cs         |   2 +-
 UnitsNet.Benchmark/UnitsNet.Benchmark.csproj  |   3 +-
 .../AbbreviatedUnitsConverter.cs              |   2 +-
 UnitsNet.Tests/QuantityTest.cs                |  72 +++-
 UnitsNet.Tests/QuantityTests.cs               |   9 +-
 UnitsNet.Tests/UnitAbbreviationsCacheTests.cs |   4 +-
 UnitsNet.Tests/UnitConverterTest.cs           |   8 +-
 UnitsNet.Tests/UnitKeyTest.cs                 | 159 ++++++++
 UnitsNet/CustomCode/Quantity.cs               | 100 +++---
 .../QuantityInfo/QuantityInfoExtensions.cs    |  16 +
 UnitsNet/CustomCode/UnitAbbreviationsCache.cs | 217 +++++------
 UnitsNet/CustomCode/UnitKey.cs                | 114 ++++++
 UnitsNet/CustomCode/UnitParser.cs             |  72 ++++
 UnitsNet/InternalHelpers/CultureHelper.cs     |   3 +-
 UnitsNet/QuantityDisplay.cs                   |  24 +-
 UnitsNet/QuantityInfo.cs                      |  17 +-
 UnitsNet/QuantityInfoLookup.cs                | 338 +++++++++++-------
 UnitsNet/QuantityNotFoundException.cs         |  33 ++
 UnitsNet/UnitConverter.cs                     | 207 ++++++-----
 UnitsNet/UnitFormatter.cs                     |   2 +-
 UnitsNet/UnitInfo.cs                          |  17 +
 UnitsNet/UnitsNet.csproj                      |   3 +
 27 files changed, 1323 insertions(+), 447 deletions(-)
 create mode 100644 UnitsNet.Benchmark/Conversions/FromString/QuantityFromStringBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitAbbreviationBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Conversions/ToString/ToStringWithDefaultPrecisionBenchmarks.cs
 create mode 100644 UnitsNet.Tests/UnitKeyTest.cs
 create mode 100644 UnitsNet/CustomCode/UnitKey.cs
 create mode 100644 UnitsNet/QuantityNotFoundException.cs

diff --git a/Directory.Packages.props b/Directory.Packages.props
index e8a9ef4c66..54479369db 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
   </PropertyGroup>
   <ItemGroup>
+    <PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.0" />
     <PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
     <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
diff --git a/UnitsNet.Benchmark/Conversions/FromString/QuantityFromStringBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromStringBenchmarks.cs
new file mode 100644
index 0000000000..8abb153032
--- /dev/null
+++ b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromStringBenchmarks.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Conversions.FromString;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class QuantityFromStringBenchmarks
+{
+    private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
+    private static readonly string ValueToParse = 123.456.ToString(Culture);
+
+    private readonly Random _random = new(42);
+    private string[] _quantitiesToParse;
+
+    [Params(1000)]
+    public int NbAbbreviations { get; set; }
+
+    [GlobalSetup(Target = nameof(FromMassString))]
+    public void PrepareMassStrings()
+    {
+        // can't have "mg" or "g" (see Acceleration.StandardGravity) and who knows what more...
+        _quantitiesToParse = _random.GetItems(["kg", "lbs", "Mlbs"], NbAbbreviations).Select(abbreviation => $"{ValueToParse} {abbreviation}").ToArray();
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeUnitAbbreviation))]
+    public void PrepareVolumeStrings()
+    {
+        _quantitiesToParse = _random.GetItems(["ml", "l", "cm³", "m³"], NbAbbreviations).Select(abbreviation => $"{ValueToParse} {abbreviation}").ToArray();;
+    }
+
+    [GlobalSetup(Target = nameof(FromPressureUnitAbbreviation))]
+    public void PreparePressureUnits()
+    {
+        _quantitiesToParse = _random.GetRandomAbbreviations<PressureUnit>(UnitsNetSetup.Default.UnitAbbreviations, NbAbbreviations).Select(abbreviation => $"{ValueToParse} {abbreviation}").ToArray();;
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeFlowUnitAbbreviation))]
+    public void PrepareVolumeFlowUnits()
+    {
+        // can't have "bpm" (see Frequency)
+        _quantitiesToParse =
+            _random.GetItems(
+                UnitsNetSetup.Default.UnitAbbreviations.GetAllUnitAbbreviationsForQuantity(typeof(VolumeFlowUnit)).Where(x => x != "bpm").ToArray(),
+                NbAbbreviations).Select(abbreviation => $"{ValueToParse} {abbreviation}").ToArray();
+    }
+
+    [Benchmark(Baseline = true)]
+    public IQuantity FromMassString()
+    {
+        IQuantity quantity = null;
+        foreach (var quantityString in _quantitiesToParse)
+        {
+            quantity = Quantity.Parse(Culture, typeof(Mass), quantityString);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var quantityString in _quantitiesToParse)
+        {
+            quantity = Quantity.Parse(Culture, typeof(Volume), quantityString);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromPressureUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var quantityString in _quantitiesToParse)
+        {
+            quantity = Quantity.Parse(Culture, typeof(Pressure), quantityString);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeFlowUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var quantityString in _quantitiesToParse)
+        {
+            quantity = Quantity.Parse(Culture, typeof(VolumeFlow), quantityString);
+        }
+
+        return quantity;
+    }
+}
diff --git a/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitAbbreviationBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitAbbreviationBenchmarks.cs
new file mode 100644
index 0000000000..9a6b3ae576
--- /dev/null
+++ b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitAbbreviationBenchmarks.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Conversions.FromString;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class QuantityFromUnitAbbreviationBenchmarks
+{
+    private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
+    private readonly Random _random = new(42);
+    private string[] _massUnits;
+    private string[] _pressureUnits;
+    private string[] _volumeFlowUnits;
+    private string[] _volumeUnits = [];
+
+    [Params(1000)]
+    public int NbAbbreviations { get; set; }
+
+    [GlobalSetup(Target = nameof(FromMassUnitAbbreviation))]
+    public void PrepareMassUnits()
+    {
+        // can't have "mg" or "g" (see Acceleration.StandardGravity) and who knows what more...
+        _massUnits = _random.GetItems(["kg", "lbs", "Mlbs"], NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeUnitAbbreviation))]
+    public void PrepareVolumeUnits()
+    {
+        _volumeUnits = _random.GetItems(["ml", "l", "cm³", "m³"], NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromPressureUnitAbbreviation))]
+    public void PreparePressureUnits()
+    {
+        _pressureUnits = _random.GetRandomAbbreviations<PressureUnit>(UnitsNetSetup.Default.UnitAbbreviations, NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeFlowUnitAbbreviation))]
+    public void PrepareVolumeFlowUnits()
+    {
+        // can't have "bpm" (see Frequency)
+        _volumeFlowUnits =
+            _random.GetItems(
+                UnitsNetSetup.Default.UnitAbbreviations.GetAllUnitAbbreviationsForQuantity(typeof(VolumeFlowUnit)).Where(x => x != "bpm").ToArray(),
+                NbAbbreviations);
+    }
+
+    [Benchmark(Baseline = true)]
+    public IQuantity FromMassUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitToParse in _massUnits)
+        {
+            quantity = Quantity.FromUnitAbbreviation(Culture, 1, unitToParse);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitToParse in _volumeUnits)
+        {
+            quantity = Quantity.FromUnitAbbreviation(Culture, 1, unitToParse);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromPressureUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitToParse in _pressureUnits)
+        {
+            quantity = Quantity.FromUnitAbbreviation(Culture, 1, unitToParse);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeFlowUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitToParse in _volumeFlowUnits)
+        {
+            quantity = Quantity.FromUnitAbbreviation(Culture, 1, unitToParse);
+        }
+
+        return quantity;
+    }
+}
diff --git a/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
new file mode 100644
index 0000000000..0d6c61bb63
--- /dev/null
+++ b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace UnitsNet.Benchmark.Conversions.FromString;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class QuantityFromUnitNameBenchmarks
+{
+    private readonly Random _random = new(42);
+    private string[] _unitNames;
+
+    [Params(1000)]
+    public int NbAbbreviations { get; set; }
+
+    [GlobalSetup(Target = nameof(FromMassUnitName))]
+    public void PrepareMassUnits()
+    {
+        _unitNames = _random.GetItems(Mass.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeUnitAbbreviation))]
+    public void PrepareVolumeUnits()
+    {
+        _unitNames = _random.GetItems(Volume.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromPressureUnitAbbreviation))]
+    public void PreparePressureUnits()
+    {
+        _unitNames = _random.GetItems(Pressure.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
+    }
+
+    [GlobalSetup(Target = nameof(FromVolumeFlowUnitAbbreviation))]
+    public void PrepareVolumeFlowUnits()
+    {
+        _unitNames = _random.GetItems(VolumeFlow.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
+    }
+
+    [Benchmark(Baseline = true)]
+    public IQuantity FromMassUnitName()
+    {
+        IQuantity quantity = null;
+        foreach (var unitName in _unitNames)
+        {
+            quantity = Quantity.From(1, nameof(Mass), unitName);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitName in _unitNames)
+        {
+            quantity = Quantity.From(1, nameof(Volume), unitName);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromPressureUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitName in _unitNames)
+        {
+            quantity = Quantity.From(1, nameof(Pressure), unitName);
+        }
+
+        return quantity;
+    }
+
+    [Benchmark(Baseline = false)]
+    public IQuantity FromVolumeFlowUnitAbbreviation()
+    {
+        IQuantity quantity = null;
+        foreach (var unitName in _unitNames)
+        {
+            quantity = Quantity.From(1, nameof(VolumeFlow), unitName);
+        }
+
+        return quantity;
+    }
+}
diff --git a/UnitsNet.Benchmark/Conversions/ToString/ToStringWithDefaultPrecisionBenchmarks.cs b/UnitsNet.Benchmark/Conversions/ToString/ToStringWithDefaultPrecisionBenchmarks.cs
new file mode 100644
index 0000000000..6d0560ebfb
--- /dev/null
+++ b/UnitsNet.Benchmark/Conversions/ToString/ToStringWithDefaultPrecisionBenchmarks.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Conversions.ToString;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class ToStringWithDefaultPrecisionBenchmarks
+{
+    private static readonly double Value = 123.456;
+    private readonly Random _random = new(42);
+
+    private Mass[] _masses = [];
+    private VolumeFlow[] _volumeFlows = [];
+
+    [Params(1000)]
+    public int NbConversions { get; set; }
+
+    [Params("G", "S", "E", "N", "A")]
+    public string Format { get; set; }
+
+    [GlobalSetup(Target = nameof(MassToString))]
+    public void PrepareMassesToTest()
+    {
+        _masses = _random.GetRandomQuantities<Mass, MassUnit>(Value, Mass.Units, NbConversions).ToArray();
+    }
+
+    [GlobalSetup(Target = nameof(VolumeFlowToString))]
+    public void PrepareVolumeFlowsToTest()
+    {
+        _volumeFlows = _random.GetRandomQuantities<VolumeFlow, VolumeFlowUnit>(Value, VolumeFlow.Units, NbConversions).ToArray();
+    }
+
+    [Benchmark(Baseline = true)]
+    public void MassToString()
+    {
+        foreach (Mass quantity in _masses)
+        {
+            var result = quantity.ToString(Format, CultureInfo.InvariantCulture);
+        }
+    }
+
+    [Benchmark]
+    public void VolumeFlowToString()
+    {
+        foreach (VolumeFlow quantity in _volumeFlows)
+        {
+            var result = quantity.ToString(Format, CultureInfo.InvariantCulture);
+        }
+    }
+}
diff --git a/UnitsNet.Benchmark/Conversions/ToValue/ConvertValueBenchmarks.cs b/UnitsNet.Benchmark/Conversions/ToValue/ConvertValueBenchmarks.cs
index 5c3ca7586c..ff02b0a189 100644
--- a/UnitsNet.Benchmark/Conversions/ToValue/ConvertValueBenchmarks.cs
+++ b/UnitsNet.Benchmark/Conversions/ToValue/ConvertValueBenchmarks.cs
@@ -51,7 +51,7 @@ public double ConvertFromQuantity()
     [GlobalSetup]
     public void PrepareTo_ConvertWith_FullyCachedFrozenDictionary()
     {
-        var nbQuantities = Quantity.Infos.Length;
+        var nbQuantities = Quantity.Infos.Count;
     }
     
 }
diff --git a/UnitsNet.Benchmark/UnitsNet.Benchmark.csproj b/UnitsNet.Benchmark/UnitsNet.Benchmark.csproj
index a8e9e2d59b..cbcef5c885 100644
--- a/UnitsNet.Benchmark/UnitsNet.Benchmark.csproj
+++ b/UnitsNet.Benchmark/UnitsNet.Benchmark.csproj
@@ -1,7 +1,8 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFrameworks>net8.0;net48</TargetFrameworks>
+    <LangVersion>preview</LangVersion>
     <Version>4.0.0.0</Version>
     <AssemblyVersion>4.0.0.0</AssemblyVersion>
     <AssemblyTitle>UnitsNet.Benchmark</AssemblyTitle>
diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
index 97cebf9eb9..11977d3f67 100644
--- a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
+++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
@@ -232,7 +232,7 @@ protected virtual Enum FindUnit(string unitAbbreviation, out QuantityInfo quanti
         /// <returns>The default abbreviation as provided by the associated <see cref="UnitAbbreviationsCache" /></returns>
         protected string GetUnitAbbreviation(Enum unit)
         {
-            return _abbreviations.GetDefaultAbbreviation(unit.GetType(), Convert.ToInt32(unit), CultureInfo.InvariantCulture);
+            return _abbreviations.GetDefaultAbbreviation(unit, CultureInfo.InvariantCulture);
         }
 
         /// <summary>
diff --git a/UnitsNet.Tests/QuantityTest.cs b/UnitsNet.Tests/QuantityTest.cs
index 3e9ca33dbe..0f47fd13c4 100644
--- a/UnitsNet.Tests/QuantityTest.cs
+++ b/UnitsNet.Tests/QuantityTest.cs
@@ -37,6 +37,13 @@ public void TryFrom_GivenNaNOrInfinity_ReturnsTrueAndQuantity(double value)
             Assert.NotNull(parsedLength);
         }
 
+        [Fact]
+        public void TryFrom_GivenNullUnit_ReturnsFalse()
+        {
+            Enum? nullUnit = null;
+            Assert.False(Quantity.TryFrom(1, nullUnit, out IQuantity? _));
+        }
+
         [Fact]
         public void From_GivenValueAndUnit_ReturnsQuantity()
         {
@@ -68,7 +75,7 @@ public void Infos_ReturnsKnownQuantityInfoObjects()
             var infos = Quantity.Infos;
 
             Assert.Superset(knownQuantityInfos.ToHashSet(), infos.ToHashSet());
-            Assert.Equal(QuantityCount, infos.Length);
+            Assert.Equal(QuantityCount, infos.Count);
         }
 
         [Fact]
@@ -91,9 +98,9 @@ public void TryGetUnitInfo_ReturnsUnitInfoForUnitEnumValue()
         }
 
         [Fact]
-        public void GetUnitInfo_ThrowsKeyNotFoundExceptionIfNotFound()
+        public void GetUnitInfo_ThrowsUnitNotFoundExceptionIfNotFound()
         {
-            Assert.Throws<KeyNotFoundException>(() => Quantity.GetUnitInfo(ConsoleColor.Red));
+            Assert.Throws<UnitNotFoundException>(() => Quantity.GetUnitInfo(ConsoleColor.Red));
         }
 
         [Fact]
@@ -111,6 +118,41 @@ public void Parse_GivenValueAndUnit_ReturnsQuantity()
             Assert.Equal(Pressure.FromMegabars(3), Quantity.Parse(InvariantCulture, typeof(Pressure), "3.0 Mbar"));
         }
 
+        [Fact]
+        public void Parse_GivenInvalidType_ThrowsArgumentException()
+        {
+            Assert.Throws<ArgumentException>(() => Quantity.Parse(typeof(bool), "3 cm"));
+        }
+
+        [Theory]
+        [InlineData(123.45, "G", LengthUnit.Centimeter)]
+        [InlineData(1.234e-8, "E", PressureUnit.Millibar)]
+        public void Parse_WithDefaultCulture_ReturnsQuantity(double value, string format, Enum unit)
+        {
+            IQuantity expectedQuantity = Quantity.From(value, unit);
+            var valueAsString = expectedQuantity.ToString(format, null);
+            Type targetType = expectedQuantity.QuantityInfo.QuantityType;
+
+            IQuantity parsedQuantity = Quantity.Parse(targetType, valueAsString);
+            
+            Assert.Equal(expectedQuantity, parsedQuantity);
+        }
+
+        [Theory]
+        [InlineData(123.45, "G", LengthUnit.Centimeter)]
+        [InlineData(1.234e-8, "E", PressureUnit.Millibar)]
+        public void TryParse_WithDefaultCulture_ReturnsQuantity(double value, string format, Enum unit)
+        {
+            IQuantity expectedQuantity = Quantity.From(value, unit);
+            var valueAsString = expectedQuantity.ToString(format, null);
+            Type targetType = expectedQuantity.QuantityInfo.QuantityType;
+
+            var success = Quantity.TryParse(targetType, valueAsString, out IQuantity? parsedQuantity);
+
+            Assert.True(success);
+            Assert.Equal(expectedQuantity, parsedQuantity);
+        }
+
         [Fact]
         public void QuantityNames_ReturnsKnownNames()
         {
@@ -119,7 +161,7 @@ public void QuantityNames_ReturnsKnownNames()
             var names = Quantity.Names;
 
             Assert.Superset(knownNames.ToHashSet(), names.ToHashSet());
-            Assert.Equal(QuantityCount, names.Length);
+            Assert.Equal(QuantityCount, names.Count);
         }
 
         [Fact]
@@ -177,5 +219,27 @@ public void Types_ReturnsKnownQuantityTypes()
 
             Assert.Superset(knownQuantities.ToHashSet(), types.ToHashSet());
         }
+    
+        [Theory]
+        [InlineData(1, 0, 0, 0, 0, 0, 0)]
+        [InlineData(0, 1, 0, 0, 0, 0, 0)]
+        [InlineData(0, 0, 1, 0, 0, 0, 0)]
+        [InlineData(0, 0, 0, 1, 0, 0, 0)]
+        [InlineData(0, 0, 0, 0, 1, 0, 0)]
+        [InlineData(0, 0, 0, 0, 0, 1, 0)]
+        [InlineData(0, 0, 0, 0, 0, 0, 1)]
+        [InlineData(0, 0, 0, 0, 0, 0, 0)]
+        public void GetQuantitiesWithBaseDimensions_ReturnsTheExpectedQuantityInfos(int length, int mass, int time, int current, int temperature, int amount, int luminousIntensity)
+        {
+            var baseDimensions = new BaseDimensions(length, mass, time, current, temperature, amount, luminousIntensity);
+            Assert.All(Quantity.GetQuantitiesWithBaseDimensions(baseDimensions), info => Assert.True(info.BaseDimensions == baseDimensions));
+            Assert.NotEmpty(Quantity.GetQuantitiesWithBaseDimensions(baseDimensions));
+        }
+
+        [Fact]
+        public void GetQuantitiesWithBaseDimensions_WithNull_ThrowsArgumentNullException()
+        {
+            Assert.Throws<ArgumentNullException>(() => Quantity.GetQuantitiesWithBaseDimensions(null!));
+        }
     }
 }
diff --git a/UnitsNet.Tests/QuantityTests.cs b/UnitsNet.Tests/QuantityTests.cs
index 7af958ab80..22154965fb 100644
--- a/UnitsNet.Tests/QuantityTests.cs
+++ b/UnitsNet.Tests/QuantityTests.cs
@@ -139,10 +139,15 @@ void AssertFrom(string quantityName, string unitName, Enum expectedUnit)
         }
 
         [Fact]
-        public void From_InvalidQuantityNameOrUnitName_ThrowsUnitNotFoundException()
+        public void From_InvalidQuantityName_ThrowsQuantityNotFoundException()
+        {
+            Assert.Throws<QuantityNotFoundException>(() => Quantity.From(5, "InvalidQuantity", "Kilogram"));
+        }
+        
+        [Fact]
+        public void From_InvalidUnitName_ThrowsUnitNotFoundException()
         {
             Assert.Throws<UnitNotFoundException>(() => Quantity.From(5, "Length", "InvalidUnit"));
-            Assert.Throws<UnitNotFoundException>(() => Quantity.From(5, "InvalidQuantity", "Kilogram"));
         }
 
         [Fact]
diff --git a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
index 26cb2a085e..5ba4305d72 100644
--- a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
+++ b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
@@ -295,10 +295,10 @@ public void ToString_WithRussianCulture()
         }
 
         [Fact]
-        public void GetDefaultAbbreviationThrowsNotImplementedExceptionIfNoneExist()
+        public void GetDefaultAbbreviationThrowsUnitNotFoundExceptionIfNoneExist()
         {
             var unitAbbreviationCache = new UnitAbbreviationsCache();
-            Assert.Throws<NotImplementedException>(() => unitAbbreviationCache.GetDefaultAbbreviation(HowMuchUnit.AShitTon));
+            Assert.Throws<UnitNotFoundException>(() => unitAbbreviationCache.GetDefaultAbbreviation(HowMuchUnit.AShitTon));
         }
 
         [Fact]
diff --git a/UnitsNet.Tests/UnitConverterTest.cs b/UnitsNet.Tests/UnitConverterTest.cs
index fe7ae8c10c..17ffdb2305 100644
--- a/UnitsNet.Tests/UnitConverterTest.cs
+++ b/UnitsNet.Tests/UnitConverterTest.cs
@@ -148,9 +148,9 @@ public void ConvertByName_UnitTypeCaseInsensitive()
 
         [Theory]
         [InlineData(1, "UnknownQuantity", "Meter", "Centimeter")]
-        public void ConvertByName_ThrowsUnitNotFoundExceptionOnUnknownQuantity(double inputValue, string quantityTypeName, string fromUnit, string toUnit)
+        public void ConvertByName_ThrowsQuantityNotFoundExceptionOnUnknownQuantity(double inputValue, string quantityTypeName, string fromUnit, string toUnit)
         {
-            Assert.Throws<UnitNotFoundException>(() => UnitConverter.ConvertByName(inputValue, quantityTypeName, fromUnit, toUnit));
+            Assert.Throws<QuantityNotFoundException>(() => UnitConverter.ConvertByName(inputValue, quantityTypeName, fromUnit, toUnit));
         }
 
         [Theory]
@@ -195,9 +195,9 @@ public void ConvertByAbbreviation_ConvertsTheValueToGivenUnit(double expectedVal
 
         [Theory]
         [InlineData(1, "UnknownQuantity", "m", "cm")]
-        public void ConvertByAbbreviation_ThrowsUnitNotFoundExceptionOnUnknownQuantity( double inputValue, string quantityTypeName, string fromUnit, string toUnit)
+        public void ConvertByAbbreviation_ThrowsQuantityNotFoundExceptionOnUnknownQuantity( double inputValue, string quantityTypeName, string fromUnit, string toUnit)
         {
-            Assert.Throws<UnitNotFoundException>(() => UnitConverter.ConvertByAbbreviation(inputValue, quantityTypeName, fromUnit, toUnit));
+            Assert.Throws<QuantityNotFoundException>(() => UnitConverter.ConvertByAbbreviation(inputValue, quantityTypeName, fromUnit, toUnit));
         }
 
         [Theory]
diff --git a/UnitsNet.Tests/UnitKeyTest.cs b/UnitsNet.Tests/UnitKeyTest.cs
new file mode 100644
index 0000000000..a064d35356
--- /dev/null
+++ b/UnitsNet.Tests/UnitKeyTest.cs
@@ -0,0 +1,159 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+using System.Reflection;
+using Xunit;
+
+namespace UnitsNet.Tests;
+
+public class UnitKeyTest
+{
+    public enum TestUnit
+    {
+        Unit1 = 1,
+        Unit2 = 2,
+        Unit3 = 3
+    }
+
+    [Theory]
+    [InlineData(1)]
+    [InlineData(2)]
+    [InlineData(3)]
+    public void Constructor_ShouldCreateUnitKey(int unitValue)
+    {
+        var unitKey = new UnitKey(typeof(TestUnit), unitValue);
+        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
+        Assert.Equal(unitValue, unitKey.UnitValue);
+    }
+
+    [Fact]
+    public void Constructor_WithNullType_ShouldNotThrow()
+    {
+        var unitKey = new UnitKey(null!, 0);
+        Assert.Null(unitKey.UnitType);
+        Assert.Equal(0, unitKey.UnitValue);
+    }
+
+    [Fact]
+    public void ConstructingWithInitializer_ShouldAssignFields()
+    {
+        var unitKey = new UnitKey { UnitType = typeof(TestUnit), UnitValue = 42 };
+        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
+        Assert.Equal(42, unitKey.UnitValue);
+    }
+
+    [Theory]
+    [InlineData(TestUnit.Unit1)]
+    [InlineData(TestUnit.Unit2)]
+    [InlineData(TestUnit.Unit3)]
+    public void ForUnit_ShouldCreateUnitKey(TestUnit unit)
+    {
+        var unitKey = UnitKey.ForUnit(unit);
+        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
+        Assert.Equal((int)unit, unitKey.UnitValue);
+    }
+
+    [Theory]
+    [InlineData(1)]
+    [InlineData(2)]
+    [InlineData(3)]
+    public void Create_ShouldCreateUnitKey(int unitValue)
+    {
+        var unitKey = UnitKey.Create<TestUnit>(unitValue);
+        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
+        Assert.Equal(unitValue, unitKey.UnitValue);
+    }
+
+    [Theory]
+    [InlineData(TestUnit.Unit1)]
+    [InlineData(TestUnit.Unit2)]
+    [InlineData(TestUnit.Unit3)]
+    public void ImplicitConversion_ShouldCreateUnitKey(TestUnit unit)
+    {
+        UnitKey unitKey = unit;
+        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
+        Assert.Equal((int)unit, unitKey.UnitValue);
+    }
+
+    [Theory]
+    [InlineData(TestUnit.Unit1)]
+    [InlineData(TestUnit.Unit2)]
+    [InlineData(TestUnit.Unit3)]
+    public void ExplicitConversion_ShouldReturnEnum(TestUnit unit)
+    {
+        var unitKey = UnitKey.ForUnit(unit);
+        var result = (TestUnit)(Enum)unitKey;
+        Assert.Equal(unit, result);
+    }
+
+    [Theory]
+    [InlineData(TestUnit.Unit1)]
+    [InlineData(TestUnit.Unit2)]
+    [InlineData(TestUnit.Unit3)]
+    public void ToUnit_ShouldReturnEnum(TestUnit unit)
+    {
+        var unitKey = UnitKey.ForUnit(unit);
+        TestUnit result = unitKey.ToUnit<TestUnit>();
+        Assert.Equal(unit, result);
+    }
+
+    [Fact]
+    public void Default_InitializesWithoutAType()
+    {
+        var defaultUnitKey = default(UnitKey);
+        Assert.Null(defaultUnitKey.UnitType);
+        Assert.Equal(0, defaultUnitKey.UnitValue);
+    }
+
+    [Fact]
+    public void Default_Equals_UnitKeyForUnit_ReturnsFalse()
+    {
+        var defaultUnitKey = default(UnitKey);
+        var unitKey = UnitKey.ForUnit(TestUnit.Unit1);
+        Assert.NotEqual(unitKey, defaultUnitKey);
+    }
+
+    [Fact]
+    public void Default_GetHashCode_ReturnsZero()
+    {
+        var defaultUnitKey = default(UnitKey);
+        Assert.Equal(0, defaultUnitKey.GetHashCode());
+    }
+
+    [Fact]
+    public void ToUnit_ShouldThrowInvalidOperationExceptionForMismatchedType()
+    {
+        var unitKey = UnitKey.ForUnit(TestUnit.Unit1);
+        Assert.Throws<InvalidOperationException>(() => unitKey.ToUnit<DayOfWeek>());
+    }
+
+    [Fact]
+    public void DefaultToUnit_ShouldThrowInvalidOperationExceptionForMismatchedType()
+    {
+        var defaultUnitKey = default(UnitKey);
+        Assert.Throws<InvalidOperationException>(() => defaultUnitKey.ToUnit<TestUnit>());
+    }
+
+    [Theory]
+    [InlineData(TestUnit.Unit1, "TestUnit.Unit1")]
+    [InlineData(TestUnit.Unit2, "TestUnit.Unit2")]
+    [InlineData(TestUnit.Unit3, "TestUnit.Unit3")]
+    [InlineData((TestUnit)(-1), "UnitType: UnitsNet.Tests.UnitKeyTest+TestUnit, UnitValue = -1")]
+    public void GetDebuggerDisplay_ShouldReturnCorrectString(TestUnit unit, string expectedDisplay)
+    {
+        var unitKey = UnitKey.ForUnit(unit);
+        var display = unitKey.GetType().GetMethod("GetDebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)!
+            .Invoke(unitKey, null);
+        Assert.Equal(expectedDisplay, display);
+    }
+
+    [Fact]
+    public void GetDebuggerDisplayWithDefault_ShouldReturnCorrectString()
+    {
+        var defaultUnitKey = default(UnitKey);
+        var display = defaultUnitKey.GetType().GetMethod("GetDebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)!
+            .Invoke(defaultUnitKey, null);
+        Assert.Equal("UnitType: , UnitValue = 0", display);
+    }
+}
diff --git a/UnitsNet/CustomCode/Quantity.cs b/UnitsNet/CustomCode/Quantity.cs
index 29187f0f90..19d98b8f4f 100644
--- a/UnitsNet/CustomCode/Quantity.cs
+++ b/UnitsNet/CustomCode/Quantity.cs
@@ -2,42 +2,42 @@
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Linq;
 using UnitsNet.Units;
 
 namespace UnitsNet
 {
     public partial class Quantity
     {
-        private static QuantityInfoLookup Default => UnitsNetSetup.Default.QuantityInfoLookup;
+        private static QuantityInfoLookup Quantities => UnitsNetSetup.Default.QuantityInfoLookup;
+        private static UnitParser UnitParser => UnitsNetSetup.Default.UnitParser;
 
         /// <summary>
         /// All enum value names of <see cref="Infos"/>, such as "Length" and "Mass".
         /// </summary>
-        public static string[] Names { get => Default.Names; }
+        public static IReadOnlyCollection<string> Names { get => Quantities.Names; }
 
         /// <summary>
         /// All quantity information objects, such as <see cref="Length.Info"/> and <see cref="Mass.Info"/>.
         /// </summary>
-        public static QuantityInfo[] Infos => Default.Infos;
+        public static IReadOnlyCollection<QuantityInfo> Infos => Quantities.Infos;
 
         /// <summary>
         /// Get <see cref="UnitInfo"/> for a given unit enum value.
         /// </summary>
-        public static UnitInfo GetUnitInfo(Enum unitEnum) => Default.GetUnitInfo(unitEnum);
+        public static UnitInfo GetUnitInfo(Enum unitEnum) => Quantities.GetUnitInfo(unitEnum);
 
         /// <summary>
         /// Try to get <see cref="UnitInfo"/> for a given unit enum value.
         /// </summary>
         public static bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unitInfo) =>
-            Default.TryGetUnitInfo(unitEnum, out unitInfo);
+            Quantities.TryGetUnitInfo(unitEnum, out unitInfo);
 
         /// <summary>
         ///
         /// </summary>
         /// <param name="unit"></param>
         /// <param name="unitInfo"></param>
-        public static void AddUnitInfo(Enum unit, UnitInfo unitInfo) => Default.AddUnitInfo(unit, unitInfo);
+        public static void AddUnitInfo(Enum unit, UnitInfo unitInfo) => Quantities.AddUnitInfo(unitInfo);
 
         /// <summary>
         ///     Dynamically constructs a quantity from a numeric value and a unit enum value.
@@ -60,14 +60,15 @@ public static IQuantity From(double value, Enum unit)
         /// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
         /// <param name="unitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
         /// <returns>An <see cref="IQuantity"/> object.</returns>
-        /// <exception cref="ArgumentException">Unit value is not a known unit enum type.</exception>
+        /// <exception cref="QuantityNotFoundException">
+        ///     Thrown when no quantity information is found for the specified quantity name.
+        /// </exception>
+        /// <exception cref="UnitNotFoundException">
+        ///     Thrown when no unit is found for the specified quantity name and unit name.
+        /// </exception>
         public static IQuantity From(double value, string quantityName, string unitName)
         {
-            // Get enum value for this unit, f.ex. LengthUnit.Meter for unit name "Meter".
-            return UnitConverter.TryParseUnit(quantityName, unitName, out Enum? unitValue) &&
-                   TryFrom(value, unitValue, out IQuantity? quantity)
-                ? quantity
-                : throw new UnitNotFoundException($"Unit [{unitName}] not found for quantity [{quantityName}].");
+            return From(value, Quantities.GetUnitByName(quantityName, unitName).Value);
         }
 
         /// <summary>
@@ -105,20 +106,7 @@ public static IQuantity From(double value, string quantityName, string unitName)
         /// <exception cref="AmbiguousUnitParseException">Multiple units found matching the given unit abbreviation.</exception>
         public static IQuantity FromUnitAbbreviation(IFormatProvider? formatProvider, double value, string unitAbbreviation)
         {
-            // TODO Optimize this with UnitValueAbbreviationLookup via UnitAbbreviationsCache.TryGetUnitValueAbbreviationLookup.
-            List<Enum> units = GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
-            if (units.Count > 1)
-            {
-                throw new AmbiguousUnitParseException($"Multiple units found matching the given unit abbreviation: {unitAbbreviation}");
-            }
-
-            if (units.Count == 0)
-            {
-                throw new UnitNotFoundException($"Unit abbreviation {unitAbbreviation} is not known. Did you pass in a custom unit abbreviation defined outside the UnitsNet library? This is currently not supported.");
-            }
-
-            Enum unit = units.Single();
-            return From(value, unit);
+            return From(value, UnitParser.GetUnitFromAbbreviation(unitAbbreviation, formatProvider).Value);
         }
 
         /// <summary>
@@ -131,10 +119,13 @@ public static IQuantity FromUnitAbbreviation(IFormatProvider? formatProvider, do
         /// <returns><c>True</c> if successful with <paramref name="quantity"/> assigned the value, otherwise <c>false</c>.</returns>
         public static bool TryFrom(double value, string quantityName, string unitName, [NotNullWhen(true)] out IQuantity? quantity)
         {
-            quantity = default;
-
-            return UnitConverter.TryParseUnit(quantityName, unitName, out Enum? unitValue) &&
-                   TryFrom(value, unitValue, out quantity);
+            if (Quantities.TryGetUnitByName(quantityName, unitName, out UnitInfo? unitInfo))
+            {
+                return TryFrom(value, unitInfo.Value, out quantity);
+            }
+            
+            quantity = null;
+            return false;
         }
 
         /// <summary>
@@ -173,20 +164,17 @@ public static bool TryFromUnitAbbreviation(double value, string unitAbbreviation
         /// <exception cref="ArgumentException">Unit value is not a known unit enum type.</exception>
         public static bool TryFromUnitAbbreviation(IFormatProvider? formatProvider, double value, string unitAbbreviation, [NotNullWhen(true)] out IQuantity? quantity)
         {
-            // TODO Optimize this with UnitValueAbbreviationLookup via UnitAbbreviationsCache.TryGetUnitValueAbbreviationLookup.
-            List<Enum> units = GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
-            if (units.Count == 1)
+            if (UnitParser.TryGetUnitFromAbbreviation(unitAbbreviation, formatProvider, out UnitInfo? unitInfo))
             {
-                Enum? unit = units.SingleOrDefault();
-                return TryFrom(value, unit, out quantity);
+                return TryFrom(value, unitInfo.Value, out quantity);
             }
 
-            quantity = default;
+            quantity = null;
             return false;
         }
 
         /// <inheritdoc cref="Parse(IFormatProvider, System.Type,string)"/>
-        public static IQuantity Parse(Type quantityType, string quantityString) => Default.Parse(null, quantityType, quantityString);
+        public static IQuantity Parse(Type quantityType, string quantityString) => Parse(null, quantityType, quantityString);
 
         /// <summary>
         ///     Dynamically parse a quantity string representation.
@@ -199,12 +187,22 @@ public static bool TryFromUnitAbbreviation(IFormatProvider? formatProvider, doub
         /// <exception cref="UnitNotFoundException">Type must be of type UnitsNet.IQuantity -or- Type is not a known quantity type.</exception>
         public static IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, string quantityString)
         {
-            return Default.Parse(formatProvider, quantityType, quantityString);
+            // TODO Support custom units (via the QuantityParser), currently only hardcoded built-in quantities are supported.
+            if (!typeof(IQuantity).IsAssignableFrom(quantityType))
+                throw new ArgumentException($"Type {quantityType} must be of type UnitsNet.IQuantity.");
+
+            if (TryParse(formatProvider, quantityType, quantityString, out IQuantity? quantity))
+                return quantity;
+
+            throw new UnitNotFoundException($"Quantity string '{quantityString}' could not be parsed to quantity '{quantityType}'.");
         }
 
         /// <inheritdoc cref="TryParse(IFormatProvider,System.Type,string,out UnitsNet.IQuantity)"/>
-        public static bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity) =>
-            Default.TryParse(quantityType, quantityString, out quantity);
+        public static bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity)
+        {
+            // TODO Support custom units (via the QuantityParser), currently only hardcoded built-in quantities are supported.
+            return TryParse(null, quantityType, quantityString, out quantity);
+        }
 
         /// <summary>
         ///     Get a list of quantities that has the given base dimensions.
@@ -212,25 +210,7 @@ public static bool TryParse(Type quantityType, string quantityString, [NotNullWh
         /// <param name="baseDimensions">The base dimensions to match.</param>
         public static IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(BaseDimensions baseDimensions)
         {
-            return Default.GetQuantitiesWithBaseDimensions(baseDimensions);
-        }
-
-        private static List<Enum> GetUnitsForAbbreviation(IFormatProvider? formatProvider, string unitAbbreviation)
-        {
-            // Use case-sensitive match to reduce ambiguity.
-            // Don't use UnitParser.TryParse() here, since it allows case-insensitive match per quantity as long as there are no ambiguous abbreviations for
-            // units of that quantity, but here we try all quantities and this results in too high of a chance for ambiguous matches,
-            // such as "cm" matching both LengthUnit.Centimeter (cm) and MolarityUnit.CentimolePerLiter (cM).
-            return Infos
-                .SelectMany(i => i.UnitInfos)
-                .Select(ui => UnitsNetSetup.Default.UnitAbbreviations
-                    .GetUnitAbbreviations(ui.Value.GetType(), Convert.ToInt32(ui.Value), formatProvider)
-                    .Contains(unitAbbreviation, StringComparer.Ordinal)
-                    ? ui.Value
-                    : null)
-                .Where(unitValue => unitValue != null)
-                .Select(unitValue => unitValue!)
-                .ToList();
+            return Infos.GetQuantitiesWithBaseDimensions(baseDimensions);
         }
     }
 }
diff --git a/UnitsNet/CustomCode/QuantityInfo/QuantityInfoExtensions.cs b/UnitsNet/CustomCode/QuantityInfo/QuantityInfoExtensions.cs
index 06b09a5878..8d74808c67 100644
--- a/UnitsNet/CustomCode/QuantityInfo/QuantityInfoExtensions.cs
+++ b/UnitsNet/CustomCode/QuantityInfo/QuantityInfoExtensions.cs
@@ -9,6 +9,22 @@ namespace UnitsNet;
 /// </summary>
 internal static class QuantityInfoExtensions
 {
+    /// <summary>
+    ///     Get a list of quantities having the given base dimensions.
+    /// </summary>
+    /// <param name="quantityInfos">The type of quantity mapping information.</param>
+    /// <param name="baseDimensions">The base dimensions to match.</param>
+    public static IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(this IEnumerable<QuantityInfo> quantityInfos,
+        BaseDimensions baseDimensions)
+    {
+        if (baseDimensions is null)
+        {
+            throw new ArgumentNullException(nameof(baseDimensions));
+        }
+
+        return quantityInfos.Where(info => info.BaseDimensions.Equals(baseDimensions));
+    }
+    
     /// <summary>
     ///     Retrieves the default unit for a specified quantity and unit system.
     /// </summary>
diff --git a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
index 6cfbff7967..723b43e348 100644
--- a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
+++ b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Linq;
 using System.Resources;
@@ -20,6 +19,16 @@ namespace UnitsNet
     /// </summary>
     public sealed class UnitAbbreviationsCache
     {
+        /// <summary>
+        ///     This key is used in the <see cref="UnitAbbreviationsCache.AbbreviationsMap" /> to uniquely identify a particular
+        ///     pair of (unit, culture), while avoiding the hash-collisions that are likely to occur when mixing different unit-types.
+        /// </summary>
+#if NET
+        private readonly record struct AbbreviationMapKey(UnitKey Unit, string CultureName);
+#else
+        private record struct AbbreviationMapKey(UnitKey Unit, string CultureName);
+#endif
+        
         /// <summary>
         ///     Fallback culture used by <see cref="GetUnitAbbreviations{TUnitType}" /> and <see cref="GetDefaultAbbreviation{TUnitType}" />
         ///     if no abbreviations are found with a given culture.
@@ -161,43 +170,49 @@ private void PerformAbbreviationMapping(Enum unitValue, IFormatProvider? formatP
             if(!QuantityInfoLookup.TryGetUnitInfo(unitValue, out UnitInfo? unitInfo))
             {
                 unitInfo = new UnitInfo(unitValue, unitValue.ToString(), BaseUnits.Undefined);
-                QuantityInfoLookup.AddUnitInfo(unitValue, unitInfo);
+                QuantityInfoLookup.AddUnitInfo(unitInfo);
             }
 
             AddAbbreviation(unitInfo, formatProvider, setAsDefault, abbreviations);
         }
-
+        
         /// <summary>
-        /// Gets the default abbreviation for a given unit. If a unit has more than one abbreviation defined, then it returns the first one.
-        /// Example: GetDefaultAbbreviation&lt;LengthUnit&gt;(LengthUnit.Kilometer) => "km"
+        ///     Gets the default abbreviation for a given unit type and its numeric enum value.
+        ///     If a unit has more than one abbreviation defined, then it returns the first one.
+        ///     Example: GetDefaultAbbreviation(LengthUnit.Centimeters, 1) => "cm"
         /// </summary>
         /// <param name="unit">The unit enum value.</param>
         /// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
         /// <typeparam name="TUnitType">The type of unit enum.</typeparam>
-        /// <returns>The default unit abbreviation string.</returns>
         public string GetDefaultAbbreviation<TUnitType>(TUnitType unit, IFormatProvider? formatProvider = null) where TUnitType : struct, Enum
         {
-            Type unitType = typeof(TUnitType);
-
-            // Edge-case: If the value was cast to Enum, it still satisfies the generic constraint so we must get the type from the value instead.
-            if (unitType == typeof(Enum)) unitType = unit.GetType();
-
-            return GetDefaultAbbreviation(unitType, Convert.ToInt32(unit), formatProvider);
+            return GetDefaultAbbreviation(UnitKey.ForUnit(unit), formatProvider);
         }
-
+        
         /// <summary>
-        /// Gets the default abbreviation for a given unit type and its numeric enum value.
-        /// If a unit has more than one abbreviation defined, then it returns the first one.
-        /// Example: GetDefaultAbbreviation&lt;LengthUnit&gt;(typeof(LengthUnit), 1) => "cm"
+        ///     Gets the default abbreviation for a given unit type and its numeric enum value.
+        ///     If a unit has more than one abbreviation defined, then it returns the first one.
+        ///     Example: GetDefaultAbbreviation&lt;LengthUnit&gt;(typeof(LengthUnit), 1) => "cm"
         /// </summary>
         /// <param name="unitType">The unit enum type.</param>
         /// <param name="unitValue">The unit enum value.</param>
         /// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
-        /// <returns>The default unit abbreviation string.</returns>
         public string GetDefaultAbbreviation(Type unitType, int unitValue, IFormatProvider? formatProvider = null)
         {
-            var abbreviations = GetUnitAbbreviations(unitType, unitValue, formatProvider);
-            return abbreviations.Length > 0 ? abbreviations[0] : string.Empty;
+            return GetDefaultAbbreviation(new UnitKey(unitType, unitValue), formatProvider);
+        }
+        
+        /// <inheritdoc cref="GetDefaultAbbreviation{TUnitType}"/>
+        /// <param name="unitKey">The key representing the unit type and value.</param>
+        /// <param name="formatProvider">
+        ///     The format provider to use for lookup. Defaults to
+        ///     <see cref="CultureInfo.CurrentCulture" /> if null.
+        /// </param>
+        /// <returns>The default unit abbreviation string.</returns>
+        public string GetDefaultAbbreviation(UnitKey unitKey, IFormatProvider? formatProvider = null)
+        {
+            var abbreviations = GetUnitAbbreviations(unitKey, formatProvider);
+            return abbreviations.Count > 0 ? abbreviations[0] : string.Empty;
         }
 
         /// <summary>
@@ -209,7 +224,7 @@ public string GetDefaultAbbreviation(Type unitType, int unitValue, IFormatProvid
         /// <returns>Unit abbreviations associated with unit.</returns>
         public string[] GetUnitAbbreviations<TUnitType>(TUnitType unit, IFormatProvider? formatProvider = null) where TUnitType : struct, Enum
         {
-            return GetUnitAbbreviations(typeof(TUnitType), Convert.ToInt32(unit), formatProvider);
+            return GetUnitAbbreviations(UnitKey.ForUnit(unit), formatProvider).ToArray();  // TODO can we change this to return an IReadonlyCollection (as the GetAbbreviations)?
         }
 
         /// <summary>
@@ -221,36 +236,37 @@ public string[] GetUnitAbbreviations<TUnitType>(TUnitType unit, IFormatProvider?
         /// <returns>Unit abbreviations associated with unit.</returns>
         public string[] GetUnitAbbreviations(Type unitType, int unitValue, IFormatProvider? formatProvider = null)
         {
-            formatProvider ??= CultureInfo.CurrentCulture;
-
-            return TryGetUnitAbbreviations(unitType, unitValue, formatProvider, out var abbreviations)
-                ? abbreviations
-                : throw new NotImplementedException($"No abbreviation is specified for {unitType.Name} with numeric value {unitValue}.");
+            return GetUnitAbbreviations(new UnitKey(unitType, unitValue), formatProvider).ToArray(); // TODO can we change this to return an IReadOnlyList (as the GetAbbreviations)?
+        }
+        
+        /// <summary>
+        /// Retrieves the unit abbreviations for a specified unit key and optional format provider.
+        /// </summary>
+        /// <param name="unitKey">The key representing the unit type and value.</param> 
+        /// <param name="formatProvider">An optional format provider to use for culture-specific formatting.</param>
+        /// <returns>A read-only collection of unit abbreviation strings.</returns>
+        public IReadOnlyList<string> GetUnitAbbreviations(UnitKey unitKey, IFormatProvider? formatProvider = null)
+        {
+            return GetAbbreviations(QuantityInfoLookup.GetUnitInfo(unitKey), formatProvider);
         }
 
         /// <summary>
         ///     Get all abbreviations for unit.
         /// </summary>
-        /// <param name="unitType">Enum type for unit.</param>
-        /// <param name="unitValue">Enum value for unit.</param>
+        /// <param name="unitKey">The unit-enum type as a hash-friendly type.</param>
         /// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
         /// <param name="abbreviations">The unit abbreviations associated with unit.</param>
         /// <returns>True if found, otherwise false.</returns>
-        private bool TryGetUnitAbbreviations(Type unitType, int unitValue, IFormatProvider? formatProvider, out string[] abbreviations)
+        private bool TryGetUnitAbbreviations(UnitKey unitKey, IFormatProvider? formatProvider, out IReadOnlyList<string> abbreviations)
         {
-            var name = Enum.GetName(unitType, unitValue);
-            var enumInstance = (Enum)Enum.Parse(unitType, name!);
-
-            if(QuantityInfoLookup.TryGetUnitInfo(enumInstance, out var unitInfo))
+            if(QuantityInfoLookup.TryGetUnitInfo(unitKey, out UnitInfo? unitInfo))
             {
-                abbreviations = GetAbbreviations(unitInfo, formatProvider!).ToArray();
+                abbreviations = GetAbbreviations(unitInfo, formatProvider);
                 return true;
             }
-            else
-            {
-                abbreviations = Array.Empty<string>();
-                return false;
-            }
+
+            abbreviations = [];
+            return false;
         }
 
         /// <summary>
@@ -261,28 +277,42 @@ private bool TryGetUnitAbbreviations(Type unitType, int unitValue, IFormatProvid
         /// <returns>Unit abbreviations associated with unit.</returns>
         public IReadOnlyList<string> GetAllUnitAbbreviationsForQuantity(Type unitEnumType, IFormatProvider? formatProvider = null)
         {
-            var enumValues = Enum.GetValues(unitEnumType).Cast<Enum>();
-            var all = GetStringUnitPairs(enumValues, formatProvider);
-            return all.Select(pair => pair.Item2).ToList();
+            var allAbbreviations = new List<string>();
+            if (!QuantityInfoLookup.TryGetQuantityByUnitType(unitEnumType, out QuantityInfo? quantityInfo))
+            {
+                var enumValues = Enum.GetValues(unitEnumType).Cast<Enum>();
+                var all = GetStringUnitPairs(enumValues, formatProvider);
+                return all.Select(pair => pair.Item2).ToList();
+            }
+            
+            foreach(UnitInfo unitInfo in quantityInfo.UnitInfos)
+            {
+                if(TryGetUnitAbbreviations(unitInfo.UnitKey, formatProvider, out IReadOnlyList<string> abbreviations))
+                {
+                    allAbbreviations.AddRange(abbreviations);
+                }
+            }
+
+            return allAbbreviations;
         }
 
         internal List<(Enum Unit, string Abbreviation)> GetStringUnitPairs(IEnumerable<Enum> enumValues, IFormatProvider? formatProvider = null)
         {
-            var ret = new List<(Enum, string)>();
+            var unitAbbreviationsPairs = new List<(Enum, string)>();
             formatProvider ??= CultureInfo.CurrentCulture;
 
             foreach(var enumValue in enumValues)
             {
-                if(TryGetUnitAbbreviations(enumValue.GetType(), Convert.ToInt32(enumValue), formatProvider, out var abbreviations))
+                if(TryGetUnitAbbreviations(enumValue, formatProvider, out var abbreviations))
                 {
                     foreach(var abbrev in abbreviations)
                     {
-                        ret.Add((enumValue, abbrev));
+                        unitAbbreviationsPairs.Add((enumValue, abbrev));
                     }
                 }
             }
 
-            return ret;
+            return unitAbbreviationsPairs;
         }
 
         /// <summary>
@@ -360,14 +390,7 @@ private static IReadOnlyList<string> AddAbbreviationsToList(bool setAsDefault, L
 
         private static AbbreviationMapKey GetAbbreviationMapKey(UnitInfo unitInfo, string cultureName)
         {
-            // TODO Enforce quantity name for custom units, optional value was required for backwards compatibility in v5.
-            // TODO Support non-enum units, using quantity name and unit name instead.
-            var unitTypeName = unitInfo.Value.GetType().FullName ?? throw new InvalidOperationException("Could not resolve unit enum type name."); // .QuantityName ?? "MissingQuantityName";
-
-            return new AbbreviationMapKey(
-                UnitTypeName: unitTypeName,
-                UnitName: unitInfo.Name,
-                CultureName: cultureName);
+            return new AbbreviationMapKey(unitInfo.UnitKey, cultureName);
         }
 
         private static string GetCultureNameOrEnglish(CultureInfo culture)
@@ -395,77 +418,33 @@ private IReadOnlyList<string> ReadAbbreviationsFromResourceFile(string? quantity
             return abbreviationsList.AsReadOnly();
         }
 
-#if NETCOREAPP
         /// <summary>
-        ///     Key for looking up unit abbreviations for a given unit and culture.
+        ///     Retrieves a list of unit information objects that match the specified unit abbreviation.
         /// </summary>
+        /// <param name="formatProvider">An optional format provider to use for culture-specific formatting.</param>
+        /// <param name="unitAbbreviation">The unit abbreviation to search for.</param>
+        /// <returns>A list of <see cref="UnitInfo" /> objects that match the specified unit abbreviation.</returns>
         /// <remarks>
-        ///     TODO Use quantity name instead of unit enum name, as part of moving from enums to string-based lookups.
+        ///     This method performs a case-sensitive match to reduce ambiguity. For example, "cm" could match both
+        ///     <c>LengthUnit.Centimeter</c> (cm) and
+        ///     <c>MolarityUnit.CentimolePerLiter</c> (cM).
         /// </remarks>
-        /// <param name="UnitTypeName">The unit enum type name, such as "UnitsNet.Units.LengthUnit" or "MyApp.HowMuchUnit".</param>
-        /// <param name="UnitName">The unit name, such as "Centimeter".</param>
-        /// <param name="CultureName">The culture name, such as "en-US".</param>
-        [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "Only used for hashing and equality.")]
-        private record AbbreviationMapKey(string UnitTypeName, string UnitName, string CultureName);
-#else
-        /// <summary>
-        ///     Key for looking up unit abbreviations for a given unit and culture.
-        /// </summary>
-        /// <remarks>
-        ///     TODO Use quantity name instead of unit enum name, as part of moving from enums to string-based lookups.
-        /// </remarks>
-        private class AbbreviationMapKey : IEquatable<AbbreviationMapKey>
+        internal List<UnitInfo> GetUnitsForAbbreviation(IFormatProvider? formatProvider, string unitAbbreviation)
         {
-            /// <summary>
-            ///     The unit enum type name, such as "UnitsNet.Units.LengthUnit" or "MyApp.HowMuchUnit".
-            /// </summary>
-            public string UnitTypeName { get; }
-
-            /// <summary>
-            ///     The unit name, such as "Centimeter".
-            /// </summary>
-            public string UnitName { get; }
-
-            /// <summary>
-            ///     The culture name, such as "en-US".
-            /// </summary>
-            public string CultureName { get; }
-
-            [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Matches record naming.")]
-            public AbbreviationMapKey(string UnitTypeName, string UnitName, string CultureName)
-            {
-                this.UnitTypeName = UnitTypeName;
-                this.UnitName = UnitName;
-                this.CultureName = CultureName;
-            }
-
-            public bool Equals(AbbreviationMapKey? other)
-            {
-                if (ReferenceEquals(null, other)) return false;
-                if (ReferenceEquals(this, other)) return true;
-                return UnitTypeName == other.UnitTypeName && UnitName == other.UnitName && CultureName == other.CultureName;
-            }
-
-            public override bool Equals(object? obj)
-            {
-                if (ReferenceEquals(null, obj)) return false;
-                if (ReferenceEquals(this, obj)) return true;
-                if (obj.GetType() != GetType()) return false;
-                return Equals((AbbreviationMapKey)obj);
-            }
-
-            public override int GetHashCode()
-            {
-                unchecked
-                {
-                    int hashCode = UnitTypeName.GetHashCode();
-                    hashCode = (hashCode * 397) ^ UnitName.GetHashCode();
-                    hashCode = (hashCode * 397) ^ CultureName.GetHashCode();
-                    return hashCode;
-                }
-            }
+            // TODO this is certain to have terrible performance (especially on the first run)
+            // TODO we should consider adding a (lazy) dictionary for these
+            // Use case-sensitive match to reduce ambiguity.
+            // Don't use UnitParser.TryParse() here, since it allows case-insensitive match per quantity as long as there are no ambiguous abbreviations for
+            // units of that quantity, but here we try all quantities and this results in too high of a chance for ambiguous matches,
+            // such as "cm" matching both LengthUnit.Centimeter (cm) and MolarityUnit.CentimolePerLiter (cM).
+            return QuantityInfoLookup.Infos
+                .SelectMany(quantityInfo => quantityInfo.UnitInfos)
+                .Select(unitInfo => GetAbbreviations(unitInfo, formatProvider).Contains(unitAbbreviation, StringComparer.Ordinal)
+                    ? unitInfo
+                    : null)
+                .Where(unitValue => unitValue != null)
+                .Select(unitValue => unitValue!)
+                .ToList();
         }
-#endif
-
     }
 }
diff --git a/UnitsNet/CustomCode/UnitKey.cs b/UnitsNet/CustomCode/UnitKey.cs
new file mode 100644
index 0000000000..bedcde65c9
--- /dev/null
+++ b/UnitsNet/CustomCode/UnitKey.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace UnitsNet;
+
+/// <summary>
+///     Represents a unique key for a unit type and its corresponding value.
+/// </summary>
+/// <remarks>
+///     This key is particularly useful when using an enum-based unit in a hash-based collection,
+///     as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
+/// </remarks>
+[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
+#if NET
+public readonly record struct UnitKey(Type UnitType, int UnitValue)
+#else
+public record struct UnitKey(Type UnitType, int UnitValue)
+#endif
+{
+    /// <summary>
+    ///     Creates a new instance of the <see cref="UnitKey" /> struct for the specified unit.
+    /// </summary>
+    /// <typeparam name="TUnit">The type of the unit, which must be a struct and an enumeration.</typeparam>
+    /// <param name="unit">The unit value to create the <see cref="UnitKey" /> for.</param>
+    /// <returns>A new instance of the <see cref="UnitKey" /> struct representing the specified unit.</returns>
+    public static UnitKey ForUnit<TUnit>(TUnit unit)
+        where TUnit : struct, Enum
+    {
+        return new UnitKey(typeof(TUnit), Unsafe.As<TUnit, int>(ref unit));
+    }
+
+    /// <summary>
+    ///     Creates a new instance of the <see cref="UnitKey" /> struct for a specified unit type and value.
+    /// </summary>
+    /// <typeparam name="TUnit">The type of the unit, which must be an enumeration.</typeparam>
+    /// <param name="unitValue">The integer value representing the unit.</param>
+    /// <returns>A new <see cref="UnitKey" /> instance representing the specified unit type and value.</returns>
+    public static UnitKey Create<TUnit>(int unitValue)
+        where TUnit : struct, Enum
+    {
+        return new UnitKey(typeof(TUnit), unitValue);
+    }
+
+    /// <summary>
+    ///     Implicitly converts an enumeration value to a <see cref="UnitKey" />.
+    /// </summary>
+    /// <param name="unit">The enumeration value to convert.</param>
+    /// <returns>A new instance of the <see cref="UnitKey" /> struct representing the specified enumeration value.</returns>
+    /// <remarks>
+    ///     This implicit conversion allows for seamless usage of enumeration values where <see cref="UnitKey" /> instances are
+    ///     expected.
+    ///     <para>
+    ///         For better performance, prefer using the <see cref="ForUnit{TUnit}(TUnit)" /> method, which avoids the boxing
+    ///         involved with the cast to <see cref="Enum" />.
+    ///     </para>
+    /// </remarks>
+    public static implicit operator UnitKey(Enum unit)
+    {
+        // using Unsafe.Unbox<int>(unit) isn't any faster
+        return new UnitKey(unit.GetType(), (int)(object)unit);
+    }
+
+    /// <summary>
+    ///     Explicitly converts a <see cref="UnitKey" /> to its corresponding enumeration value.
+    /// </summary>
+    /// <param name="unitKey">The <see cref="UnitKey" /> instance to convert.</param>
+    /// <returns>The enumeration value represented by the <see cref="UnitKey" />.</returns>
+    /// <remarks>
+    ///     This explicit conversion is useful when you need to retrieve the original enumeration value from a
+    ///     <see cref="UnitKey" />.
+    /// </remarks>
+    public static explicit operator Enum(UnitKey unitKey)
+    {
+        return (Enum)Enum.ToObject(unitKey.UnitType, unitKey.UnitValue);
+    }
+
+    /// <summary>
+    ///     Converts the current <see cref="UnitKey" /> to its corresponding enumeration value of type
+    ///     <typeparamref name="TUnit" />.
+    /// </summary>
+    /// <typeparam name="TUnit">The type of the unit, which must be a struct and an enumeration.</typeparam>
+    /// <returns>The enumeration value of type <typeparamref name="TUnit" /> represented by the current <see cref="UnitKey" />.</returns>
+    /// <exception cref="InvalidOperationException">
+    ///     Thrown when the type of <typeparamref name="TUnit" /> does not match the type of the current <see cref="UnitKey" />
+    ///     .
+    /// </exception>
+    /// <remarks>
+    ///     This method is useful for retrieving the original enumeration value from a <see cref="UnitKey" />.
+    /// </remarks>
+    public TUnit ToUnit<TUnit>() where TUnit : struct, Enum
+    {
+        if (typeof(TUnit) != UnitType)
+        {
+            throw new InvalidOperationException($"Cannot convert UnitKey of type {UnitType} to {typeof(TUnit)}.");
+        }
+
+        var unitValue = UnitValue;
+        return Unsafe.As<int, TUnit>(ref unitValue);
+    }
+
+    private string GetDebuggerDisplay()
+    {
+        try
+        {
+            var unitName = Enum.GetName(UnitType, UnitValue);
+            return string.IsNullOrEmpty(unitName) ? $"{nameof(UnitType)}: {UnitType}, {nameof(UnitValue)} = {UnitValue}" : $"{UnitType.Name}.{unitName}";
+        }
+        catch
+        {
+            return $"{nameof(UnitType)}: {UnitType}, {nameof(UnitValue)} = {UnitValue}";
+        }
+    }
+}
diff --git a/UnitsNet/CustomCode/UnitParser.cs b/UnitsNet/CustomCode/UnitParser.cs
index 77f96b1018..53d63bcfa5 100644
--- a/UnitsNet/CustomCode/UnitParser.cs
+++ b/UnitsNet/CustomCode/UnitParser.cs
@@ -237,5 +237,77 @@ public bool TryParse([NotNullWhen(true)] string? unitAbbreviation, Type unitType
             (Enum Unit, string Abbreviation)[] caseSensitiveMatches = stringUnitPairs.Where(pair => pair.Abbreviation.Equals(unitAbbreviation)).ToArray();
             return caseSensitiveMatches.Length == 0 ? matches : caseSensitiveMatches;
         }
+
+        /// <summary>
+        ///     Retrieves the unit information from the given unit abbreviation.
+        /// </summary>
+        /// <remarks>
+        ///     This method is currently not optimized for performance and will enumerate all units and their unit abbreviations
+        ///     each time.<br />
+        ///     Unit abbreviation matching in the <see cref="TryParse{TUnitType}(string?,out TUnitType)" />
+        ///     overload is case-insensitive.<br />
+        ///     <br />
+        ///     This will fail if more than one unit across all quantities share the same unit abbreviation.<br />
+        /// </remarks>
+        /// <param name="unitAbbreviation">The unit abbreviation to parse.</param>
+        /// <param name="formatProvider">The format provider to use for culture-specific formatting. Can be null.</param>
+        /// <returns>The unit information corresponding to the given unit abbreviation.</returns>
+        /// <exception cref="UnitNotFoundException">
+        ///     Thrown when the unit abbreviation is not recognized as a valid unit for the specified culture.
+        /// </exception>
+        /// <exception cref="AmbiguousUnitParseException">
+        ///     Thrown when multiple units are found matching the given unit abbreviation.
+        /// </exception>
+        internal UnitInfo GetUnitFromAbbreviation(string unitAbbreviation, IFormatProvider? formatProvider)
+        {
+            List<UnitInfo> units = _unitAbbreviationsCache.GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
+            return units.Count switch
+            {
+                0 => throw new UnitNotFoundException(
+                    $"The unit abbreviation '{unitAbbreviation}' is not recognized as a valid unit for the specified culture."),
+                1 => units[0],
+                _ => throw new AmbiguousUnitParseException(
+                    $"Cannot parse \"{unitAbbreviation}\" since it matches multiple units: {string.Join(", ", units.Select(x => x.Name).OrderBy(x => x))}.")
+            };
+        }
+
+        /// <summary>
+        ///     Attempts to parse the specified unit abbreviation into an <see cref="UnitInfo" /> object.
+        /// </summary>
+        /// <remarks>
+        ///     This method is currently not optimized for performance and will enumerate all units and their unit abbreviations
+        ///     each time.<br />
+        ///     Unit abbreviation matching in the <see cref="TryParse{TUnitType}(string,out TUnitType)" />
+        ///     overload is case-insensitive.<br />
+        ///     <br />
+        ///     This will fail if more than one unit across all quantities share the same unit abbreviation.<br />
+        /// </remarks>
+        /// <param name="unitAbbreviation">The unit abbreviation to parse.</param>
+        /// <param name="formatProvider">The format provider to use for parsing, or <c>null</c> to use the current culture.</param>
+        /// <param name="unit">
+        ///     When this method returns, contains the parsed <see cref="UnitInfo" /> object if the parsing succeeded,
+        ///     or <c>null</c> if the parsing failed. This parameter is passed uninitialized.
+        /// </param>
+        /// <returns>
+        ///     <c>true</c> if the unit abbreviation was successfully parsed; otherwise, <c>false</c>.
+        /// </returns>
+        internal bool TryGetUnitFromAbbreviation([NotNullWhen(true)]string? unitAbbreviation, IFormatProvider? formatProvider, [NotNullWhen(true)] out UnitInfo? unit)
+        {
+            if (unitAbbreviation == null)
+            {
+                unit = null;
+                return false;
+            }
+
+            List<UnitInfo> units = _unitAbbreviationsCache.GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
+            if (units.Count == 1)
+            {
+                unit = units[0];
+                return true;
+            }
+
+            unit = null;
+            return false;
+        }
     }
 }
diff --git a/UnitsNet/InternalHelpers/CultureHelper.cs b/UnitsNet/InternalHelpers/CultureHelper.cs
index d8c40745c8..37931f6e88 100644
--- a/UnitsNet/InternalHelpers/CultureHelper.cs
+++ b/UnitsNet/InternalHelpers/CultureHelper.cs
@@ -1,4 +1,4 @@
-// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Licensed under MIT No Attribution, see LICENSE file at the root.
 // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
 
 using System;
@@ -10,6 +10,7 @@ namespace UnitsNet.InternalHelpers;
 /// <summary>
 ///     Helper class for <see cref="CultureInfo"/> and related operations.
 /// </summary>
+[Obsolete("string -> CultureInfo conversions are not in the scope of UnitsNet")]
 internal static class CultureHelper
 {
     private static readonly ConcurrentDictionary<string, CultureInfo> CultureCache = new();
diff --git a/UnitsNet/QuantityDisplay.cs b/UnitsNet/QuantityDisplay.cs
index 71c0d25ec5..166afc9544 100644
--- a/UnitsNet/QuantityDisplay.cs
+++ b/UnitsNet/QuantityDisplay.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
 using System.Linq;
@@ -33,29 +34,28 @@ public AbbreviationDisplay(IQuantity quantity)
             _quantity = quantity;
             QuantityInfo quantityQuantityInfo = quantity.QuantityInfo;
             IQuantity baseQuantity = quantity.ToUnit(quantityQuantityInfo.BaseUnitInfo.Value);
-            Conversions = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity, x.Value)).ToArray();
+            Conversions = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity, x)).ToArray();
         }
 
         [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-        public string DefaultAbbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(_quantity.Unit.GetType(), Convert.ToInt32(_quantity.Unit));
+        public string DefaultAbbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(_quantity.Unit);
 
         [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
-        public string[] Abbreviations =>
-            UnitsNetSetup.Default.UnitAbbreviations.GetUnitAbbreviations(_quantity.QuantityInfo.UnitType, Convert.ToInt32(_quantity.Unit));
+        public IReadOnlyList<string> Abbreviations => UnitsNetSetup.Default.UnitAbbreviations.GetUnitAbbreviations(_quantity.Unit);
 
         public ConvertedQuantity[] Conversions { get; }
 
         [DebuggerDisplay("{Abbreviation}")]
-        internal readonly struct ConvertedQuantity(IQuantity baseQuantity, Enum unit)
+        internal readonly struct ConvertedQuantity(IQuantity baseQuantity, UnitInfo unit)
         {
             [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            public Enum Unit { get; } = unit;
+            public UnitInfo Unit { get; } = unit;
 
             [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
-            public IQuantity Quantity => baseQuantity.ToUnit(Unit);
+            public IQuantity Quantity => baseQuantity.ToUnit(Unit.Value);
 
             [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            public string Abbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(Unit.GetType(), Convert.ToInt32(Unit));
+            public string Abbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(Unit.UnitKey);
 
             public override string ToString()
             {
@@ -112,7 +112,7 @@ public QuantityConvertor(IQuantity quantity)
             QuantityToString = new StringFormatsDisplay(quantity);
             QuantityInfo quantityQuantityInfo = quantity.QuantityInfo;
             IQuantity baseQuantity = quantity.ToUnit(quantityQuantityInfo.BaseUnitInfo.Value);
-            QuantityToUnit = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity.ToUnit(x.Value))).ToArray();
+            QuantityToUnit = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity.ToUnit(x.Value), x)).ToArray();
         }
 
         public StringFormatsDisplay QuantityToString { get; }
@@ -126,10 +126,10 @@ internal readonly struct StringFormatsDisplay(IQuantity quantity)
         }
 
         [DebuggerDisplay("{Quantity}")]
-        internal readonly struct ConvertedQuantity(IQuantity quantity)
+        internal readonly struct ConvertedQuantity(IQuantity quantity, UnitInfo unitInfo)
         {
-            public Enum Unit => Quantity.Unit;
-            public string Abbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(Quantity.Unit.GetType(), Convert.ToInt32(Quantity.Unit));
+            public UnitInfo Unit { get; } = unitInfo;
+            public string Abbreviation => UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(Unit.UnitKey);
             public ValueDisplay Value => new(Quantity);
             public IQuantity Quantity { get; } = quantity;
 
diff --git a/UnitsNet/QuantityInfo.cs b/UnitsNet/QuantityInfo.cs
index 6e9caa78fa..d50bf8ce69 100644
--- a/UnitsNet/QuantityInfo.cs
+++ b/UnitsNet/QuantityInfo.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using UnitsNet.Units;
 
@@ -41,7 +42,7 @@ public QuantityInfo(string name, Type unitType, UnitInfo[] unitInfos, Enum baseU
             UnitInfos = unitInfos ?? throw new ArgumentNullException(nameof(unitInfos));
 
             BaseUnitInfo = UnitInfos.First(unitInfo => unitInfo.Value.Equals(baseUnit));
-            ValueType = zero.GetType();
+            QuantityType = zero.GetType();
         }
 
         /// <summary>
@@ -68,11 +69,19 @@ public QuantityInfo(string name, Type unitType, UnitInfo[] unitInfos, Enum baseU
         ///     Unit enum type, such as <see cref="LengthUnit"/> or <see cref="MassUnit"/>.
         /// </summary>
         public Type UnitType { get; }
-
+        
         /// <summary>
-        ///     Quantity value type, such as <see cref="Length"/> or <see cref="Mass"/>.
+        ///     Quantity value type, such as <see cref="Length" /> or <see cref="Mass" />.
         /// </summary>
-        public Type ValueType { get; }
+        public Type QuantityType { get; }
+
+        /// <inheritdoc cref="QuantityType" />
+        [Obsolete("Replaced by the QuantityType property.")]
+        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+        public Type ValueType
+        {
+            get => QuantityType;
+        }
 
         /// <summary>
         ///     The <see cref="BaseDimensions" /> for a quantity.
diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs
index 386e797cf0..ce30cb2416 100644
--- a/UnitsNet/QuantityInfoLookup.cs
+++ b/UnitsNet/QuantityInfoLookup.cs
@@ -1,168 +1,234 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
 using System.Linq;
-
-namespace UnitsNet
+#if NET8_0_OR_GREATER
+using System.Collections.Frozen;
+using QuantityByTypeLookupDictionary = System.Collections.Frozen.FrozenDictionary<System.Type, UnitsNet.QuantityInfo>;
+#else
+using QuantityByTypeLookupDictionary = System.Collections.Generic.Dictionary<System.Type, UnitsNet.QuantityInfo>;
+#endif
+using UnitByKeyLookupDictionary = System.Collections.Generic.Dictionary<UnitsNet.UnitKey, UnitsNet.UnitInfo>;
+
+namespace UnitsNet;
+
+/// <summary>
+///     A collection of <see cref="QuantityInfo" />.
+/// </summary>
+/// <remarks>
+///     Access type is <c>internal</c> until this class is matured and ready for external use.
+/// </remarks>
+internal class QuantityInfoLookup
 {
+    private readonly Lazy<SortedDictionary<string, QuantityInfo>> _quantitiesByName;
+    private readonly Lazy<QuantityByTypeLookupDictionary> _quantitiesByType;
+    private readonly Lazy<QuantityByTypeLookupDictionary> _quantitiesByUnitType;
+    private readonly Lazy<UnitByKeyLookupDictionary> _unitsByKey;
+
     /// <summary>
-    /// A collection of <see cref="QuantityInfo"/>.
+    ///     Initializes a new instance of the <see cref="QuantityInfoLookup" /> class.
     /// </summary>
-    /// <remarks>
-    ///     Access type is <c>internal</c> until this class is matured and ready for external use.
-    /// </remarks>
-    internal class QuantityInfoLookup
+    /// <param name="quantityInfos">A collection of quantity information objects.</param>
+    public QuantityInfoLookup(ICollection<QuantityInfo> quantityInfos)
     {
-        private readonly Lazy<QuantityInfo[]> _infosLazy;
-        private readonly Lazy<Dictionary<(Type, string), UnitInfo>> _unitTypeAndNameToUnitInfoLazy;
-
-        /// <summary>
-        /// New instance.
-        /// </summary>
-        /// <param name="quantityInfos"></param>
-        public QuantityInfoLookup(ICollection<QuantityInfo> quantityInfos)
-        {
-            Names = quantityInfos.Select(qt => qt.Name).ToArray();
-
-            _infosLazy = new Lazy<QuantityInfo[]>(() => quantityInfos
-                .OrderBy(quantityInfo => quantityInfo.Name)
-                .ToArray());
+        _quantitiesByName = new Lazy<SortedDictionary<string, QuantityInfo>>(() =>
+            new SortedDictionary<string, QuantityInfo>(quantityInfos.ToDictionary(info => info.Name), StringComparer.OrdinalIgnoreCase));
+
+#if NET8_0_OR_GREATER
+        _quantitiesByType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToFrozenDictionary(info => info.QuantityType));
+        _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToFrozenDictionary(info => info.UnitType));
+#else
+        _quantitiesByType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToDictionary(info => info.QuantityType));
+        _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToDictionary(info => info.UnitType));
+#endif
+        _unitsByKey = new Lazy<UnitByKeyLookupDictionary>(() => quantityInfos.SelectMany(quantityInfo => quantityInfo.UnitInfos).ToDictionary(x => x.UnitKey));
+    }
 
-            _unitTypeAndNameToUnitInfoLazy = new Lazy<Dictionary<(Type, string), UnitInfo>>(() =>
-            {
-                return Infos
-                    .SelectMany(quantityInfo => quantityInfo.UnitInfos
-                        .Select(unitInfo => new KeyValuePair<(Type, string), UnitInfo>(
-                            (unitInfo.Value.GetType(), unitInfo.Name),
-                            unitInfo)))
-                    .ToDictionary(x => x.Key, x => x.Value);
-            });
-        }
+    /// <summary>
+    ///     All enum value names of <see cref="Infos" />, such as "Length" and "Mass".
+    /// </summary>
+    public IReadOnlyCollection<string> Names => _quantitiesByName.Value.Keys;
 
-        /// <summary>
-        /// All enum value names of <see cref="Infos"/>, such as "Length" and "Mass".
-        /// </summary>
-        public string[] Names { get; }
+    /// <summary>
+    ///     A read-only dictionary that maps quantity names to their corresponding <see cref="QuantityInfo" />.
+    /// </summary>
+    public IReadOnlyDictionary<string, QuantityInfo> ByName => _quantitiesByName.Value;
 
-        /// <summary>
-        /// All quantity information objects, such as <see cref="Length.Info"/> and <see cref="Mass.Info"/>.
-        /// </summary>
-        public QuantityInfo[] Infos => _infosLazy.Value;
+    /// <summary>
+    ///     All quantity information objects, such as <see cref="Length.Info" /> and <see cref="Mass.Info" />.
+    /// </summary>
+    public IReadOnlyCollection<QuantityInfo> Infos => _quantitiesByName.Value.Values;
 
-        /// <summary>
-        /// Gets the <see cref="QuantityInfo"/> for a given unit.
-        /// </summary>
-        public QuantityInfo GetQuantityInfo(UnitInfo unitInfo)
+    /// <summary>
+    ///     Retrieves the <see cref="UnitInfo" /> for a specified <see cref="UnitKey" />.
+    /// </summary>
+    /// <param name="unitKey">The key representing the unit for which information is being requested.</param>
+    /// <returns>The <see cref="UnitInfo" /> associated with the specified <paramref name="unitKey" />.</returns>
+    /// <exception cref="UnitNotFoundException">
+    ///     Thrown when no unit information is found for the specified
+    ///     <paramref name="unitKey" />.
+    /// </exception>
+    public UnitInfo GetUnitInfo(UnitKey unitKey)
+    {
+        if (!TryGetUnitInfo(unitKey, out UnitInfo? unitInfo))
         {
-            Type unitType = unitInfo.Value.GetType();
-            return _infosLazy.Value.First(i => i.UnitType == unitType);
+            throw new UnitNotFoundException($"No unit information found for the specified enum value: {unitKey}.");
         }
 
-        /// <summary>
-        /// Try to get the <see cref="QuantityInfo"/> for a given unit.
-        /// </summary>
-        public bool TryGetQuantityInfo(UnitInfo unitInfo, [NotNullWhen(true)] out QuantityInfo? quantityInfo)
-        {
-            Type unitType = unitInfo.Value.GetType();
-            if (_infosLazy.Value.FirstOrDefault(i => i.UnitType == unitType) is { } qi)
-            {
-                quantityInfo = qi;
-                return true;
-            }
+        return unitInfo;
+    }
 
-            quantityInfo = default;
-            return false;
-        }
+    /// <summary>
+    ///     Try to get <see cref="UnitInfo" /> for a given unit enum value.
+    /// </summary>
+    public bool TryGetUnitInfo(UnitKey unitKey, [NotNullWhen(true)] out UnitInfo? unitInfo)
+    {
+        return _unitsByKey.Value.TryGetValue(unitKey, out unitInfo);
+    }
+    
+    /// <summary>
+    ///
+    /// </summary>
+    /// <param name="unitInfo"></param>
+    public void AddUnitInfo(UnitInfo unitInfo)
+    {
+        _unitsByKey.Value.Add(unitInfo.UnitKey, unitInfo);
+    }
+    
+    /// <summary>
+    ///     Dynamically construct a quantity.
+    /// </summary>
+    /// <param name="value">Numeric value.</param>
+    /// <param name="unit">Unit enum value.</param>
+    /// <returns>An <see cref="IQuantity" /> object.</returns>
+    /// <exception cref="UnitNotFoundException">Unit value is not a know unit enum type.</exception>
+    public IQuantity From(double value, UnitKey unit)
+    {
+        // TODO Support custom units, currently only hardcoded built-in quantities are supported.
+        return Quantity.TryFrom(value, (Enum)unit, out IQuantity? quantity)
+            ? quantity
+            : throw new UnitNotFoundException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a custom enum type defined outside the UnitsNet library?");
+    }
+    
+    /// <summary>
+    ///     Attempts to create a quantity from the specified value and unit.
+    /// </summary>
+    /// <param name="value">The value of the quantity.</param>
+    /// <param name="unit">The unit of the quantity, represented as an <see cref="Enum" />.</param>
+    /// <param name="quantity">
+    ///     When this method returns, contains the created quantity if the conversion succeeded,
+    ///     or <c>null</c> if the conversion failed. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns>
+    ///     <c>true</c> if the quantity was successfully created; otherwise, <c>false</c>.
+    /// </returns>
+    public bool TryFrom(double value, [NotNullWhen(true)] Enum? unit, [NotNullWhen(true)] out IQuantity? quantity)
+    {
+        // TODO Support custom units, currently only hardcoded built-in quantities are supported.
+        return Quantity.TryFrom(value, unit, out quantity);
+    }
 
-        /// <summary>
-        /// Get <see cref="UnitInfo"/> for a given unit enum value.
-        /// </summary>
-        public UnitInfo GetUnitInfo(Enum unitEnum) => _unitTypeAndNameToUnitInfoLazy.Value[(unitEnum.GetType(), unitEnum.ToString())];
-
-        /// <summary>
-        /// Try to get <see cref="UnitInfo"/> for a given unit enum value.
-        /// </summary>
-        public bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unitInfo) =>
-            _unitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);
-
-        /// <summary>
-        ///
-        /// </summary>
-        /// <param name="unit"></param>
-        /// <param name="unitInfo"></param>
-        public void AddUnitInfo(Enum unit, UnitInfo unitInfo)
+    /// <summary>
+    ///     Retrieves the <see cref="QuantityInfo" /> associated with the specified quantity name.
+    /// </summary>
+    /// <param name="quantityName">The name of the quantity to retrieve information for.</param>
+    /// <returns>The <see cref="QuantityInfo" /> associated with the specified quantity name.</returns>
+    /// <exception cref="QuantityNotFoundException">
+    ///     Thrown when no quantity information is found for the specified quantity name.
+    /// </exception>
+    internal QuantityInfo GetQuantityByName(string quantityName)
+    {
+        if (!ByName.TryGetValue(quantityName, out QuantityInfo? quantityInfo))
         {
-            _unitTypeAndNameToUnitInfoLazy.Value.Add((unit.GetType(), unit.ToString()), unitInfo);
+            throw new QuantityNotFoundException($"No quantity information was found for the type: {quantityName}.")
+            {
+                Data = { ["quantityName"] = quantityName }
+            };
         }
 
-        /// <summary>
-        ///     Dynamically construct a quantity.
-        /// </summary>
-        /// <param name="value">Numeric value.</param>
-        /// <param name="unit">Unit enum value.</param>
-        /// <returns>An <see cref="IQuantity"/> object.</returns>
-        /// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
-        public IQuantity From(double value, Enum unit)
-        {
-            // TODO Support custom units, currently only hardcoded built-in quantities are supported.
-            return Quantity.TryFrom(value, unit, out IQuantity? quantity)
-                ? quantity
-                : throw new UnitNotFoundException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a custom enum type defined outside the UnitsNet library?");
-        }
+        return quantityInfo;
+    }
 
-        /// <inheritdoc cref="Quantity.TryFrom(double,System.Enum,out UnitsNet.IQuantity)"/>
-        public bool TryFrom(double value, Enum unit, [NotNullWhen(true)] out IQuantity? quantity)
-        {
-            // Implicit cast to QuantityValue would prevent TryFrom from being called,
-            // so we need to explicitly check this here for double arguments.
-            if (double.IsNaN(value) || double.IsInfinity(value))
-            {
-                quantity = default(IQuantity);
-                return false;
-            }
+    /// <summary>
+    ///     Attempts to retrieve the <see cref="QuantityInfo" /> associated with the specified quantity name.
+    /// </summary>
+    /// <param name="quantityName">The name of the quantity to look up.</param>
+    /// <param name="quantityInfo">
+    ///     When this method returns, contains the <see cref="QuantityInfo" /> associated with the specified quantity name,
+    ///     if the name is found; otherwise, <c>null</c>. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns>
+    ///     <c>true</c> if the quantity name was found; otherwise, <c>false</c>.
+    /// </returns>
+    internal bool TryGetQuantityByName(string quantityName, [NotNullWhen(true)] out QuantityInfo? quantityInfo)
+    {
+        return ByName.TryGetValue(quantityName, out quantityInfo);
+    }
 
-            // TODO Support custom units, currently only hardcoded built-in quantities are supported.
-            return Quantity.TryFrom(value, unit, out quantity);
+    /// <summary>
+    ///     Attempts to parse a unit information object based on its quantity and unit names.
+    /// </summary>
+    /// <param name="quantityName">
+    ///     The invariant quantity name, such as "Length". This parameter does not support localization.
+    /// </param>
+    /// <param name="unitName">
+    ///     The invariant unit enum name, such as "Meter". This parameter does not support localization.
+    /// </param>
+    /// <returns>
+    ///     The <see cref="UnitInfo" /> object representing the unit information.
+    /// </returns>
+    /// <exception cref="QuantityNotFoundException">
+    ///     Thrown when no quantity information is found for the specified quantity name.
+    /// </exception>
+    /// <exception cref="UnitNotFoundException">
+    ///     Thrown when no unit is found for the specified quantity name and unit name.
+    /// </exception>
+    internal UnitInfo GetUnitByName(string quantityName, string unitName)
+    {
+        QuantityInfo quantityInfo = GetQuantityByName(quantityName);
+        UnitInfo? unitInfo = quantityInfo.UnitInfos.FirstOrDefault(unit => string.Equals(unit.Name, unitName, StringComparison.OrdinalIgnoreCase));
+        return unitInfo ??
+               throw new UnitNotFoundException($"No unit was found for quantity '{quantityName}' with the name: '{unitName}'.")
+               {
+                   Data = { ["quantityName"] = quantityName, ["unitName"] = unitName }
+               };
+    }
+    
+    /// <summary>
+    ///     Attempts to parse unit information based on its quantity and unit names.
+    /// </summary>
+    /// <param name="quantityName">The invariant quantity name, such as "Length". This parameter does not support localization.</param>
+    /// <param name="unitName">The invariant unit name, such as "Meter". This parameter does not support localization.</param>
+    /// <param name="unitInfo">
+    ///     When this method returns, contains the parsed unit information if the parsing succeeded, or <c>null</c> if the
+    ///     parsing failed.
+    /// </param>
+    /// <returns><c>true</c> if the unit information was successfully parsed; otherwise, <c>false</c>.</returns>
+    internal bool TryGetUnitByName(string quantityName, string unitName, [NotNullWhen(true)] out UnitInfo? unitInfo)
+    {
+        if (!TryGetQuantityByName(quantityName, out QuantityInfo? quantityInfo))
+        {
+            unitInfo = null;
+            return false;
         }
 
-        /// <inheritdoc cref="Parse(IFormatProvider, System.Type,string)"/>
-        public IQuantity Parse(Type quantityType, string quantityString) => Parse(null, quantityType, quantityString);
-
-        /// <summary>
-        ///     Dynamically parse a quantity string representation.
-        /// </summary>
-        /// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
-        /// <param name="quantityType">Type of quantity, such as <see cref="Length"/>.</param>
-        /// <param name="quantityString">Quantity string representation, such as "1.5 kg". Must be compatible with given quantity type.</param>
-        /// <returns>The parsed quantity.</returns>
-        /// <exception cref="ArgumentException">Type must be of type UnitsNet.IQuantity -or- Type is not a known quantity type.</exception>
-        public IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, string quantityString)
-        {
-            if (!typeof(IQuantity).IsAssignableFrom(quantityType))
-                throw new ArgumentException($"Type {quantityType} must be of type UnitsNet.IQuantity.");
+        unitInfo = quantityInfo.UnitInfos.FirstOrDefault(unit => string.Equals(unit.Name, unitName, StringComparison.OrdinalIgnoreCase));
+        return unitInfo is not null;
+    }
 
-            // TODO Support custom units, currently only hardcoded built-in quantities are supported.
-            if (Quantity.TryParse(formatProvider, quantityType, quantityString, out IQuantity? quantity))
-                return quantity;
 
-            throw new UnitNotFoundException($"Quantity string '{quantityString}' could not be parsed to quantity '{quantityType}'.");
-        }
+    public bool TryGetQuantityByUnitType(Type unitType, [NotNullWhen(true)] out QuantityInfo? quantityInfo)
+    {
+        return _quantitiesByUnitType.Value.TryGetValue(unitType, out quantityInfo);
+    }
 
-        /// <inheritdoc cref="Quantity.TryParse(IFormatProvider,System.Type,string,out UnitsNet.IQuantity)"/>
-        public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity)
+    public QuantityInfo GetQuantityByUnitType(Type unitType)
+    {
+        if (TryGetQuantityByUnitType(unitType, out QuantityInfo? quantityInfo))
         {
-            // TODO Support custom units, currently only hardcoded built-in quantities are supported.
-            return Quantity.TryParse(null, quantityType, quantityString, out quantity);
+            return quantityInfo;
         }
 
-        /// <summary>
-        ///     Get a list of quantities that has the given base dimensions.
-        /// </summary>
-        /// <param name="baseDimensions">The base dimensions to match.</param>
-        public IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(BaseDimensions baseDimensions)
-        {
-            return _infosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
-        }
+        throw new UnitNotFoundException($"No quantity was found with the specified unit type: '{unitType}'.") { Data = { ["unitType"] = unitType.Name } };
     }
 }
diff --git a/UnitsNet/QuantityNotFoundException.cs b/UnitsNet/QuantityNotFoundException.cs
new file mode 100644
index 0000000000..dbd088b93c
--- /dev/null
+++ b/UnitsNet/QuantityNotFoundException.cs
@@ -0,0 +1,33 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+
+namespace UnitsNet;
+
+/// <summary>
+///     Represents an exception that is thrown when a quantity is not found.
+/// </summary>
+/// <remarks>
+///     This exception is typically encountered during dynamic conversions, such as when using
+///     <see cref="UnitConverter.ConvertByName" /> to convert units by their names.
+/// </remarks>
+public class QuantityNotFoundException : UnitsNetException
+{
+    /// <inheritdoc />
+    public QuantityNotFoundException()
+    {
+    }
+
+    /// <inheritdoc />
+    public QuantityNotFoundException(string message)
+        : base(message)
+    {
+    }
+
+    /// <inheritdoc />
+    public QuantityNotFoundException(string message, Exception innerException)
+        : base(message, innerException)
+    {
+    }
+}
diff --git a/UnitsNet/UnitConverter.cs b/UnitsNet/UnitConverter.cs
index 5a8c1e917f..0815506966 100644
--- a/UnitsNet/UnitConverter.cs
+++ b/UnitsNet/UnitConverter.cs
@@ -5,7 +5,6 @@
 using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
-using System.Linq;
 using UnitsNet.InternalHelpers;
 using UnitsNet.Units;
 
@@ -321,24 +320,25 @@ public static bool TryConvert(double fromValue, Enum fromUnitValue, Enum toUnitV
         ///     c) To unit: Meter, Centimeter etc if Length is selected
         /// </summary>
         /// <param name="fromValue">
-        ///     Input value, which together with <paramref name="fromUnit" /> represents the quantity to
+        ///     Input value, which together with <paramref name="fromUnitName" /> represents the quantity to
         ///     convert from.
         /// </param>
         /// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
-        /// <param name="fromUnit">The invariant unit enum name, such as "Meter". Does not support localization.</param>
-        /// <param name="toUnit">The invariant unit enum name, such as "Meter". Does not support localization.</param>
+        /// <param name="fromUnitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
+        /// <param name="toUnitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
         /// <example>double centimeters = ConvertByName(5, "Length", "Meter", "Centimeter"); // 500</example>
-        /// <returns>Output value as the result of converting to <paramref name="toUnit" />.</returns>
-        /// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
+        /// <returns>Output value as the result of converting to <paramref name="toUnitName" />.</returns>
+        /// <exception cref="QuantityNotFoundException">
+        ///     Thrown when no quantity information is found for the specified quantity name.
+        /// </exception>
+        /// <exception cref="UnitNotFoundException">No units match the provided unit name.</exception>
         /// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
-        public static double ConvertByName(double fromValue, string quantityName, string fromUnit, string toUnit)
+        public static double ConvertByName(double fromValue, string quantityName, string fromUnitName, string toUnitName)
         {
-            if (!TryParseUnit(quantityName, toUnit, out Enum? toUnitValue)) // ex: LengthUnit.Centimeter
-            {
-                throw new UnitNotFoundException($"Unit not found [{toUnit}] for quantity [{quantityName}].") { Data = { ["unitName"] = toUnit } };
-            }
-
-            return Quantity.From(fromValue, quantityName, fromUnit).As(toUnitValue);
+            QuantityInfoLookup quantities = UnitsNetSetup.Default.QuantityInfoLookup;
+            UnitInfo fromUnit = quantities.GetUnitByName(quantityName, fromUnitName);
+            UnitInfo toUnit = quantities.GetUnitByName(quantityName, toUnitName);
+            return Quantity.From(fromValue, fromUnit.Value).As(toUnit.Value);
         }
 
         /// <summary>
@@ -361,22 +361,17 @@ public static double ConvertByName(double fromValue, string quantityName, string
         /// <returns>True if conversion was successful.</returns>
         public static bool TryConvertByName(double inputValue, string quantityName, string fromUnit, string toUnit, out double result)
         {
+            QuantityInfoLookup quantities = UnitsNetSetup.Default.QuantityInfoLookup;
+            if (quantities.TryGetUnitByName(quantityName, fromUnit, out UnitInfo? fromUnitInfo) &&
+                quantities.TryGetUnitByName(quantityName, toUnit, out UnitInfo? toUnitInfo) &&
+                Quantity.TryFrom(inputValue, fromUnitInfo.Value, out IQuantity? quantity))
+            {
+                result = quantity.As(toUnitInfo.Value);
+                return true;
+            }
+            
             result = 0d;
-
-            if (!TryGetUnitType(quantityName, out Type? unitType))
-                return false;
-
-            if (!TryParseUnit(unitType, fromUnit, out Enum? fromUnitValue)) // ex: LengthUnit.Meter
-                return false;
-
-            if (!TryParseUnit(unitType, toUnit, out Enum? toUnitValue)) // ex: LengthUnit.Centimeter
-                return false;
-
-            if (!Quantity.TryFrom(inputValue, fromUnitValue, out IQuantity? quantity))
-                return false;
-
-            result = quantity.As(toUnitValue);
-            return true;
+            return false;
         }
 
         /// <summary>
@@ -396,9 +391,14 @@ public static bool TryConvertByName(double inputValue, string quantityName, stri
         /// <param name="toUnitAbbrev">The abbreviation of the unit in the thread's current culture, such as "m".</param>
         /// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
         /// <returns>Output value as the result of converting to <paramref name="toUnitAbbrev" />.</returns>
+        /// <exception cref="QuantityNotFoundException">
+        ///     Thrown when no quantity information is found for the specified quantity name.
+        /// </exception>
+        /// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
+        /// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
         public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev)
         {
-            return ConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, null);
+            return ConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, (IFormatProvider?)null);
         }
 
         /// <summary>
@@ -419,23 +419,51 @@ public static double ConvertByAbbreviation(double fromValue, string quantityName
         /// <param name="culture">Culture to parse abbreviations with.</param>
         /// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
         /// <returns>Output value as the result of converting to <paramref name="toUnitAbbrev" />.</returns>
-        /// <exception cref="UnitNotFoundException">
-        ///     No unit types match the prefix of <paramref name="quantityName" /> or no units
-        ///     are mapped to the abbreviation.
+        /// <exception cref="QuantityNotFoundException">
+        ///     Thrown when no quantity information is found for the specified quantity name.
         /// </exception>
+        /// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
         /// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
+        [Obsolete("Methods accepting a culture name are deprecated in favor of using an instance of the IFormatProvider.")]
         public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, string? culture)
         {
-            if (!TryGetUnitType(quantityName, out Type? unitType))
-                throw new UnitNotFoundException($"The unit type for the given quantity was not found: {quantityName}");
-
-            var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);
-
-            var fromUnit = UnitsNetSetup.Default.UnitParser.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter
-            var fromQuantity = Quantity.From(fromValue, fromUnit);
+            return ConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, CultureHelper.GetCultureOrInvariant(culture));
+        }
 
-            var toUnit = UnitsNetSetup.Default.UnitParser.Parse(toUnitAbbrev, unitType, cultureInfo); // ex:("cm", LengthUnit) => LengthUnit.Centimeter
-            return fromQuantity.As(toUnit);
+        /// <summary>
+        ///     Convert between any two quantity units by their abbreviations, such as converting a "Length" of N "m" to "cm".
+        ///     This is particularly useful for creating things like a generated unit conversion UI,
+        ///     where you list some selectors:
+        ///     a) Quantity: Length, Mass, Force etc.
+        ///     b) From unit: Meter, Centimeter etc if Length is selected
+        ///     c) To unit: Meter, Centimeter etc if Length is selected
+        /// </summary>
+        /// <param name="fromValue">
+        ///     Input value, which together with <paramref name="fromUnitAbbrev" /> represents the quantity to
+        ///     convert from.
+        /// </param>
+        /// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
+        /// <param name="fromUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
+        /// <param name="toUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
+        /// <param name="formatProvider">
+        ///     The format provider to use for lookup. Defaults to <see cref="System.Globalization.CultureInfo.CurrentCulture" />
+        ///     if null.
+        /// </param>
+        /// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
+        /// <returns>Output value as the result of converting to <paramref name="toUnitAbbrev" />.</returns>
+        /// <exception cref="QuantityNotFoundException">
+        ///     Thrown when no quantity information is found for the specified quantity name.
+        /// </exception>
+        /// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
+        /// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
+        public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, IFormatProvider? formatProvider)
+        {
+            QuantityInfoLookup quantities = UnitsNetSetup.Default.QuantityInfoLookup;
+            UnitParser unitParser = UnitsNetSetup.Default.UnitParser;
+            QuantityInfo quantityInfo = quantities.GetQuantityByName(quantityName);
+            Enum fromUnit = unitParser.Parse(fromUnitAbbrev, quantityInfo.UnitType, formatProvider); // ex: ("m", LengthUnit) => LengthUnit.Meter
+            Enum toUnit = unitParser.Parse(toUnitAbbrev, quantityInfo.UnitType, formatProvider); // ex:("cm", LengthUnit) => LengthUnit.Centimeter
+            return Quantity.From(fromValue, fromUnit).As(toUnit);
         }
 
         /// <summary>
@@ -458,7 +486,7 @@ public static double ConvertByAbbreviation(double fromValue, string quantityName
         /// <returns>True if conversion was successful.</returns>
         public static bool TryConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, out double result)
         {
-            return TryConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, out result, null);
+            return TryConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, out result, (IFormatProvider?)null);
         }
 
         /// <summary>
@@ -480,75 +508,56 @@ public static bool TryConvertByAbbreviation(double fromValue, string quantityNam
         /// <param name="result">Result if conversion was successful, 0 if not.</param>
         /// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
         /// <returns>True if conversion was successful.</returns>
+        [Obsolete("Methods accepting a culture name are deprecated in favor of using an instance of the IFormatProvider.")]
         public static bool TryConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, out double result,
             string? culture)
         {
-            result = 0d;
-
-            if (!TryGetUnitType(quantityName, out Type? unitType))
-                return false;
-
-            var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);
-
-            if (!UnitsNetSetup.Default.UnitParser.TryParse(fromUnitAbbrev, unitType, cultureInfo, out Enum? fromUnit)) // ex: ("m", LengthUnit) => LengthUnit.Meter
-                return false;
-
-            if (!UnitsNetSetup.Default.UnitParser.TryParse(toUnitAbbrev, unitType, cultureInfo, out Enum? toUnit)) // ex:("cm", LengthUnit) => LengthUnit.Centimeter
-                return false;
-
-            var fromQuantity = Quantity.From(fromValue, fromUnit);
-            result = fromQuantity.As(toUnit);
-
-            return true;
-        }
-
-        /// <summary>
-        ///     Try to parse a unit by the unit enum type <paramref name="unitType" /> and a unit enum value <paramref name="unitName" />>
-        /// </summary>
-        /// <param name="unitType">Unit type, such as <see cref="LengthUnit" />.</param>
-        /// <param name="unitName">Unit name, such as "Meter" corresponding to <see cref="LengthUnit.Meter" />.</param>
-        /// <param name="unitValue">The return enum value, such as <see cref="LengthUnit.Meter" /> boxed as an object.</param>
-        /// <returns>True if succeeded, otherwise false.</returns>
-        /// <exception cref="UnitNotFoundException">No unit values match the <paramref name="unitName" />.</exception>
-        // TODO Move to Quantity.
-        internal static bool TryParseUnit(Type unitType, string unitName, [NotNullWhen(true)] out Enum? unitValue)
-        {
-            unitValue = null;
-            var eNames = Enum.GetNames(unitType);
-            var matchedUnitName = eNames.FirstOrDefault(x => x.Equals(unitName, StringComparison.OrdinalIgnoreCase));
-            if (matchedUnitName == null)
-                return false;
-
-            unitValue = (Enum) Enum.Parse(unitType, matchedUnitName);
-            return true;
+            return TryConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, out result, CultureHelper.GetCultureOrInvariant(culture));
         }
 
         /// <summary>
-        ///     Try to parse a unit enum value by its quantity and unit names.
+        ///     Convert between any two quantity units by their abbreviations, such as converting a "Length" of N "m" to "cm".
+        ///     This is particularly useful for creating things like a generated unit conversion UI,
+        ///     where you list some selectors:
+        ///     a) Quantity: Length, Mass, Force etc.
+        ///     b) From unit: Meter, Centimeter etc if Length is selected
+        ///     c) To unit: Meter, Centimeter etc if Length is selected
         /// </summary>
+        /// <param name="fromValue">
+        ///     Input value, which together with <paramref name="fromUnitAbbrev" /> represents the quantity to
+        ///     convert from.
+        /// </param>
         /// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
-        /// <param name="unitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
-        /// <param name="unitValue">The return enum value, such as <see cref="LengthUnit.Meter" /> boxed as an object.</param>
-        /// <returns>True if succeeded, otherwise false.</returns>
-        /// <exception cref="UnitNotFoundException">No unit values match the <paramref name="unitName" />.</exception>
-        // TODO Move to Quantity.
-        internal static bool TryParseUnit(string quantityName, string unitName, [NotNullWhen(true)] out Enum? unitValue)
+        /// <param name="fromUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
+        /// <param name="toUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
+        /// <param name="formatProvider">
+        ///     The format provider to use for lookup. Defaults to <see cref="System.Globalization.CultureInfo.CurrentCulture" />
+        ///     if null.
+        /// </param>
+        /// <param name="result">Result if conversion was successful, 0 if not.</param>
+        /// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
+        /// <returns>True if conversion was successful.</returns>
+        public static bool TryConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, out double result,
+            IFormatProvider? formatProvider)
         {
-            unitValue = default;
+            QuantityInfoLookup quantities = UnitsNetSetup.Default.QuantityInfoLookup;
+            UnitParser unitParser = UnitsNetSetup.Default.UnitParser;
+            if (!quantities.TryGetQuantityByName(quantityName, out QuantityInfo? quantityInfo) )
+            {
+                result = 0;
+                return false;
+            }
 
-            // Get enum type for unit of this quantity, f.ex. LengthUnit for quantity Length.
-            // Then try to parse the unit enum value.
-            return TryGetUnitType(quantityName, out Type? unitType) &&
-                   TryParseUnit(unitType, unitName, out unitValue);
-        }
+            if (!unitParser.TryParse(fromUnitAbbrev, quantityInfo.UnitType, formatProvider, out Enum? fromUnit) ||
+                !unitParser.TryParse(toUnitAbbrev, quantityInfo.UnitType, formatProvider, out Enum? toUnit))
+            {
+                result = 0;
+                return false;
+            }
 
-        // TODO Move to Quantity.
-        internal static bool TryGetUnitType(string quantityName, [NotNullWhen(true)] out Type? unitType)
-        {
-            var quantityInfo = Quantity.Infos.FirstOrDefault(info => info.Name.Equals(quantityName, StringComparison.OrdinalIgnoreCase));
+            result = Quantity.From(fromValue, fromUnit).As(toUnit);
+            return true;
 
-            unitType = quantityInfo?.UnitType;
-            return quantityInfo != null;
         }
     }
 }
diff --git a/UnitsNet/UnitFormatter.cs b/UnitsNet/UnitFormatter.cs
index 18f9979c72..df359557e6 100644
--- a/UnitsNet/UnitFormatter.cs
+++ b/UnitsNet/UnitFormatter.cs
@@ -73,7 +73,7 @@ private static bool NearlyEqual(double a, double b)
         public static object[] GetFormatArgs<TUnitType>(TUnitType unit, double value, IFormatProvider? culture, IEnumerable<object> args)
             where TUnitType : struct, Enum
         {
-            string abbreviation = UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(typeof(TUnitType), Convert.ToInt32(unit), culture);
+            string abbreviation = UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(unit, culture);
             return new object[] {value, abbreviation}.Concat(args).ToArray();
         }
     }
diff --git a/UnitsNet/UnitInfo.cs b/UnitsNet/UnitInfo.cs
index 576b5c6d4d..7a7adc6819 100644
--- a/UnitsNet/UnitInfo.cs
+++ b/UnitsNet/UnitInfo.cs
@@ -2,6 +2,7 @@
 // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
 
 using System;
+using System.Diagnostics;
 using UnitsNet.Units;
 
 namespace UnitsNet
@@ -72,6 +73,15 @@ public UnitInfo(Enum value, string pluralName, BaseUnits baseUnits, string quant
         /// Name of the quantity this unit belongs to. May be null for custom units.
         /// </summary>
         public string? QuantityName { get; }
+
+        /// <summary>
+        ///     Gets the unique key representing the unit type and its corresponding value.
+        /// </summary>
+        /// <remarks>
+        ///     This key is particularly useful when using an enum-based unit in a hash-based collection,
+        ///     as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
+        /// </remarks>
+        public virtual UnitKey UnitKey => Value;
     }
 
     /// <inheritdoc cref="UnitInfo" />
@@ -81,6 +91,7 @@ public UnitInfo(Enum value, string pluralName, BaseUnits baseUnits, string quant
     ///     or dynamically via <see cref="IQuantity{TUnitType}.QuantityInfo" />.
     /// </remarks>
     /// <typeparam name="TUnit">The unit enum type, such as <see cref="LengthUnit" />. </typeparam>
+    [DebuggerDisplay("{Name} ({Value})")]
     public class UnitInfo<TUnit> : UnitInfo
         where TUnit : struct, Enum
     {
@@ -101,5 +112,11 @@ public UnitInfo(TUnit value, string pluralName, BaseUnits baseUnits, string quan
 
         /// <inheritdoc cref="UnitInfo.Value"/>
         public new TUnit Value { get; }
+        
+        /// <inheritdoc />
+        public override UnitKey UnitKey
+        {
+            get => UnitKey.ForUnit(Value);
+        }
     }
 }
diff --git a/UnitsNet/UnitsNet.csproj b/UnitsNet/UnitsNet.csproj
index 865beb7390..7527d2f40d 100644
--- a/UnitsNet/UnitsNet.csproj
+++ b/UnitsNet/UnitsNet.csproj
@@ -53,4 +53,7 @@
     <EmbeddedResource Include="GeneratedCode\Resources\*.restext" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
+    <PackageReference Include="System.Runtime.CompilerServices.Unsafe" />
+  </ItemGroup>
 </Project>

From 03973bf6f86e3fb2c703d134a12a4571fcca39fa Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 31 Jan 2025 20:19:29 +0200
Subject: [PATCH 2/6] - QuantityInfoLookup: replaced the
 ICollection<QuantityInfo> parameter with IReadOnlyCollection<QuantityInfo> -
 UnitsNetSetup: replaced the ICollection<QuantityInfo> parameter with
 IReadOnlyCollection<QuantityInfo> - UnitAbbreviationsCache: introduced a
 constructor with a list of quantities, and improve the comments of the other
 constructors - added tests and benchmarks covering the UnitAbbreviations
 initializations

---
 .../QuantityFromUnitNameBenchmarks.cs         | 12 +--
 ...reviationsCacheInitializationBenchmarks.cs | 71 +++++++++++++++
 UnitsNet.Tests/UnitAbbreviationsCacheTests.cs | 86 ++++++++++++++++++-
 UnitsNet/CustomCode/UnitAbbreviationsCache.cs | 59 ++++++-------
 UnitsNet/CustomCode/UnitsNetSetup.cs          |  5 +-
 UnitsNet/QuantityInfoLookup.cs                |  2 +-
 6 files changed, 195 insertions(+), 40 deletions(-)
 create mode 100644 UnitsNet.Benchmark/Initializations/UnitAbbreviationsCacheInitializationBenchmarks.cs

diff --git a/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
index 0d6c61bb63..f337295e9d 100644
--- a/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
+++ b/UnitsNet.Benchmark/Conversions/FromString/QuantityFromUnitNameBenchmarks.cs
@@ -22,19 +22,19 @@ public void PrepareMassUnits()
         _unitNames = _random.GetItems(Mass.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
     }
 
-    [GlobalSetup(Target = nameof(FromVolumeUnitAbbreviation))]
+    [GlobalSetup(Target = nameof(FromVolumeUnitName))]
     public void PrepareVolumeUnits()
     {
         _unitNames = _random.GetItems(Volume.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
     }
 
-    [GlobalSetup(Target = nameof(FromPressureUnitAbbreviation))]
+    [GlobalSetup(Target = nameof(FromPressureUnitName))]
     public void PreparePressureUnits()
     {
         _unitNames = _random.GetItems(Pressure.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
     }
 
-    [GlobalSetup(Target = nameof(FromVolumeFlowUnitAbbreviation))]
+    [GlobalSetup(Target = nameof(FromVolumeFlowUnitName))]
     public void PrepareVolumeFlowUnits()
     {
         _unitNames = _random.GetItems(VolumeFlow.Info.UnitInfos.Select(x => x.Name).ToArray(), NbAbbreviations);
@@ -53,7 +53,7 @@ public IQuantity FromMassUnitName()
     }
 
     [Benchmark(Baseline = false)]
-    public IQuantity FromVolumeUnitAbbreviation()
+    public IQuantity FromVolumeUnitName()
     {
         IQuantity quantity = null;
         foreach (var unitName in _unitNames)
@@ -65,7 +65,7 @@ public IQuantity FromVolumeUnitAbbreviation()
     }
 
     [Benchmark(Baseline = false)]
-    public IQuantity FromPressureUnitAbbreviation()
+    public IQuantity FromPressureUnitName()
     {
         IQuantity quantity = null;
         foreach (var unitName in _unitNames)
@@ -77,7 +77,7 @@ public IQuantity FromPressureUnitAbbreviation()
     }
 
     [Benchmark(Baseline = false)]
-    public IQuantity FromVolumeFlowUnitAbbreviation()
+    public IQuantity FromVolumeFlowUnitName()
     {
         IQuantity quantity = null;
         foreach (var unitName in _unitNames)
diff --git a/UnitsNet.Benchmark/Initializations/UnitAbbreviationsCacheInitializationBenchmarks.cs b/UnitsNet.Benchmark/Initializations/UnitAbbreviationsCacheInitializationBenchmarks.cs
new file mode 100644
index 0000000000..26e6d9722b
--- /dev/null
+++ b/UnitsNet.Benchmark/Initializations/UnitAbbreviationsCacheInitializationBenchmarks.cs
@@ -0,0 +1,71 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Initializations;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class UnitAbbreviationsCacheInitializationBenchmarks
+{
+    [GlobalSetup]
+    public void InitializeUnitsNetSetup()
+    {
+        var quantities = Quantity.Infos.Count;
+    }
+
+    [Benchmark(Baseline = true)]
+    public string Default()
+    {
+        var cache = UnitAbbreviationsCache.CreateDefault();
+        return cache.GetDefaultAbbreviation(MassUnit.Gram);
+    }
+
+    [Benchmark]
+    public string EmptyWithCustomMapping()
+    {
+        var cache = new UnitAbbreviationsCache();
+        cache.MapUnitToDefaultAbbreviation(MassUnit.Gram, "zz");
+        return cache.GetDefaultAbbreviation(MassUnit.Gram);
+    }
+
+    [Benchmark]
+    public string WithSpecificQuantity()
+    {
+        var cache = new UnitAbbreviationsCache([Mass.Info]);
+        return cache.GetDefaultAbbreviation(MassUnit.Gram);
+    }
+
+    [Benchmark]
+    public string WithSpecificQuantityAndCustomMapping()
+    {
+        var cache = new UnitAbbreviationsCache([Mass.Info]);
+        cache.MapUnitToDefaultAbbreviation(MassUnit.Gram, "zz");
+        return cache.GetDefaultAbbreviation(MassUnit.Gram);
+    }
+
+    [Benchmark]
+    public string DefaultWithoutLookup()
+    {
+        var cache = UnitAbbreviationsCache.CreateDefault();
+        return cache.GetAbbreviations(Mass.Info.BaseUnitInfo)[0];
+    }
+
+    [Benchmark]
+    public string EmptyWithoutLookup()
+    {
+        var cache = new UnitAbbreviationsCache();
+        return cache.GetAbbreviations(Mass.Info.BaseUnitInfo)[0];
+    }
+
+    [Benchmark]
+    public string WithSpecificQuantityWithoutLookup()
+    {
+        var cache = new UnitAbbreviationsCache([Mass.Info]);
+        return cache.GetAbbreviations(Mass.Info.BaseUnitInfo)[0];
+    }
+}
diff --git a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
index 5ba4305d72..e80090518a 100644
--- a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
+++ b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
@@ -3,7 +3,9 @@
 
 using System;
 using System.Globalization;
+using System.Linq;
 using UnitsNet.Tests.CustomQuantities;
+using UnitsNet.Tests.Helpers;
 using UnitsNet.Units;
 using Xunit;
 
@@ -294,11 +296,54 @@ public void ToString_WithRussianCulture()
             Assert.Equal("1 м³", Volume.FromCubicMeters(1).ToUnit(VolumeUnit.CubicMeter).ToString(RussianCulture));
         }
 
+        [Fact]
+        public void UnitAbbreviationsCacheDefaultReturnsUnitsNetSetupDefaultUnitAbbreviations()
+        {
+            Assert.Equal(UnitsNetSetup.Default.UnitAbbreviations, UnitAbbreviationsCache.Default);
+        }
+
+        [Fact]
+        public void GetUnitAbbreviationsThrowsUnitNotFoundExceptionIfNoneExist()
+        {
+            Assert.Multiple(checks: [
+                () => Assert.Throws<UnitNotFoundException>(() => new UnitAbbreviationsCache().GetUnitAbbreviations(MassUnit.Gram)),
+                () => Assert.Throws<UnitNotFoundException>(() => new UnitAbbreviationsCache().GetUnitAbbreviations(typeof(MassUnit), (int)MassUnit.Gram))
+            ]);
+        }
+
         [Fact]
         public void GetDefaultAbbreviationThrowsUnitNotFoundExceptionIfNoneExist()
         {
-            var unitAbbreviationCache = new UnitAbbreviationsCache();
-            Assert.Throws<UnitNotFoundException>(() => unitAbbreviationCache.GetDefaultAbbreviation(HowMuchUnit.AShitTon));
+            Assert.Multiple(checks: [
+                () => Assert.Throws<UnitNotFoundException>(() => new UnitAbbreviationsCache().GetDefaultAbbreviation(MassUnit.Gram)),
+                () => Assert.Throws<UnitNotFoundException>(() => new UnitAbbreviationsCache().GetDefaultAbbreviation(typeof(MassUnit), (int)MassUnit.Gram))
+            ]);
+        }
+
+        [Fact]
+        public void GetUnitAbbreviationsReturnsTheExpectedAbbreviationWhenConstructedWithTheSpecificQuantityInfo()
+        {
+            Assert.Multiple(checks:
+            [
+                () => { Assert.Equal("g", new UnitAbbreviationsCache([Mass.Info]).GetUnitAbbreviations(MassUnit.Gram, AmericanCulture)[0]); },
+                () => { Assert.Equal("g", new UnitAbbreviationsCache([Mass.Info]).GetUnitAbbreviations(typeof(MassUnit), (int)MassUnit.Gram, AmericanCulture)[0]); }
+            ]);
+        }
+
+        [Fact]
+        public void GetDefaultAbbreviationReturnsTheExpectedAbbreviationWhenConstructedWithTheSpecificQuantityInfo()
+        {
+            Assert.Multiple(checks:
+            [
+                () => { Assert.Equal("g", new UnitAbbreviationsCache([Mass.Info]).GetDefaultAbbreviation(MassUnit.Gram, AmericanCulture)); },
+                () => { Assert.Equal("g", new UnitAbbreviationsCache([Mass.Info]).GetDefaultAbbreviation(typeof(MassUnit), (int)MassUnit.Gram, AmericanCulture)); }
+            ]);
+        }
+
+        [Fact]
+        public void GetAbbreviationsThrowsArgumentNullExceptionWhenGivenANullUnitInfo()
+        {
+            Assert.Throws<ArgumentNullException>(() => new UnitAbbreviationsCache().GetAbbreviations(null!));
         }
 
         [Fact]
@@ -355,6 +400,30 @@ public void MapUnitToDefaultAbbreviation_GivenUnitAndCulture_SetsDefaultAbbrevia
             Assert.Equal("m^2", cache.GetDefaultAbbreviation(AreaUnit.SquareMeter, AmericanCulture));
         }
 
+        [Fact]
+        public void MapUnitToDefaultAbbreviation_GivenUnitAndNoCulture_SetsDefaultAbbreviationForUnitForCurrentCulture()
+        {
+            using var cultureScope = new CultureScope(NorwegianCultureName);
+            var cache = new UnitAbbreviationsCache([Mass.Info]);
+
+            cache.MapUnitToDefaultAbbreviation(MassUnit.Gram, "zz");
+
+            Assert.Equal("zz", cache.GetDefaultAbbreviation(MassUnit.Gram));
+            Assert.Equal("g", cache.GetDefaultAbbreviation(MassUnit.Gram, AmericanCulture));
+        }
+
+        [Fact]
+        public void MapUnitToDefaultAbbreviation_GivenUnitTypeAndValue_SetsDefaultAbbreviationForUnitForCurrentCulture()
+        {
+            using var cultureScope = new CultureScope(NorwegianCultureName);
+            var cache = new UnitAbbreviationsCache([Mass.Info]);
+
+            cache.MapUnitToDefaultAbbreviation(typeof(MassUnit), (int)MassUnit.Gram, null, "zz");
+
+            Assert.Equal("zz", cache.GetDefaultAbbreviation(MassUnit.Gram));
+            Assert.Equal("g", cache.GetDefaultAbbreviation(MassUnit.Gram, AmericanCulture));
+        }
+
         [Fact]
         public void MapUnitToDefaultAbbreviation_GivenCustomAbbreviation_SetsAbbreviationUsedByQuantityToString()
         {
@@ -365,6 +434,19 @@ public void MapUnitToDefaultAbbreviation_GivenCustomAbbreviation_SetsAbbreviatio
             Assert.Equal("1 m^2", Area.FromSquareMeters(1).ToString(newZealandCulture));
         }
 
+        [Fact]
+        public void MapUnitToAbbreviation_GivenUnitTypeAndValue_AddsTheAbbreviationForUnitForCurrentCulture()
+        {
+            using var cultureScope = new CultureScope(NorwegianCultureName);
+            var cache = new UnitAbbreviationsCache([Mass.Info]);
+
+            cache.MapUnitToAbbreviation(typeof(MassUnit), (int)MassUnit.Gram, null, "zz");
+
+            Assert.Equal("zz", cache.GetUnitAbbreviations(MassUnit.Gram).Last());
+            Assert.Equal("g", cache.GetDefaultAbbreviation(MassUnit.Gram, AmericanCulture));
+            Assert.DoesNotContain("zz", cache.GetUnitAbbreviations(MassUnit.Gram, AmericanCulture));
+        }
+
         [Fact]
         public void MapUnitToAbbreviation_DoesNotInsertDuplicates()
         {
diff --git a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
index 723b43e348..0c59494db4 100644
--- a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
+++ b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
@@ -8,6 +8,7 @@
 using System.Linq;
 using System.Resources;
 using UnitsNet.Units;
+using AbbreviationMapKey = System.ValueTuple<UnitsNet.UnitKey, string>;
 
 // ReSharper disable once CheckNamespace
 namespace UnitsNet
@@ -19,16 +20,6 @@ namespace UnitsNet
     /// </summary>
     public sealed class UnitAbbreviationsCache
     {
-        /// <summary>
-        ///     This key is used in the <see cref="UnitAbbreviationsCache.AbbreviationsMap" /> to uniquely identify a particular
-        ///     pair of (unit, culture), while avoiding the hash-collisions that are likely to occur when mixing different unit-types.
-        /// </summary>
-#if NET
-        private readonly record struct AbbreviationMapKey(UnitKey Unit, string CultureName);
-#else
-        private record struct AbbreviationMapKey(UnitKey Unit, string CultureName);
-#endif
-        
         /// <summary>
         ///     Fallback culture used by <see cref="GetUnitAbbreviations{TUnitType}" /> and <see cref="GetDefaultAbbreviation{TUnitType}" />
         ///     if no abbreviations are found with a given culture.
@@ -63,9 +54,18 @@ public UnitAbbreviationsCache()
             : this(new QuantityInfoLookup([]))
         {
         }
-
+        
         /// <summary>
-        ///     Creates an instance of the cache and load all the abbreviations defined in the library.
+        ///     Creates an instance of the cache using the specified set of quantities.
+        /// </summary>
+        /// <returns>Instance for mapping the units of the provided quantities.</returns>
+        public UnitAbbreviationsCache(IReadOnlyCollection<QuantityInfo> quantities)
+            :this(new QuantityInfoLookup(quantities))
+        {
+        }
+        
+        /// <summary>
+        ///     Creates an instance of the cache using the specified set of quantities.
         /// </summary>
         /// <remarks>
         ///     Access type is <c>internal</c> until this class is matured and ready for external use.
@@ -74,12 +74,12 @@ internal UnitAbbreviationsCache(QuantityInfoLookup quantityInfoLookup)
         {
             QuantityInfoLookup = quantityInfoLookup;
         }
-
+        
         /// <summary>
-        ///     Create an instance of the cache and load all the built-in unit abbreviations defined in the library.
+        ///     Create an instance of the cache and load all the built-in quantities defined in the library.
         /// </summary>
-        /// <returns>Instance with default abbreviations cache.</returns>
-        public static UnitAbbreviationsCache CreateDefault() => new(new QuantityInfoLookup(Quantity.ByName.Values));
+        /// <returns>Instance for mapping any of the built-in units.</returns>
+        public static UnitAbbreviationsCache CreateDefault() => new(new QuantityInfoLookup(Quantity.Infos));
 
         /// <summary>
         /// Adds one or more unit abbreviation for the given unit enum value.
@@ -91,7 +91,7 @@ internal UnitAbbreviationsCache(QuantityInfoLookup quantityInfoLookup)
         /// <typeparam name="TUnitType">The type of unit enum.</typeparam>
         public void MapUnitToAbbreviation<TUnitType>(TUnitType unit, params string[] abbreviations) where TUnitType : struct, Enum
         {
-            PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, false, abbreviations);
+            PerformAbbreviationMapping(UnitKey.ForUnit(unit), CultureInfo.CurrentCulture, false, abbreviations);
         }
 
         /// <summary>
@@ -104,7 +104,7 @@ public void MapUnitToAbbreviation<TUnitType>(TUnitType unit, params string[] abb
         /// <typeparam name="TUnitType">The type of unit enum.</typeparam>
         public void MapUnitToDefaultAbbreviation<TUnitType>(TUnitType unit, string abbreviation) where TUnitType : struct, Enum
         {
-            PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, true, abbreviation);
+            PerformAbbreviationMapping(UnitKey.ForUnit(unit), CultureInfo.CurrentCulture, true, abbreviation);
         }
 
         /// <summary>
@@ -118,7 +118,7 @@ public void MapUnitToDefaultAbbreviation<TUnitType>(TUnitType unit, string abbre
         /// <typeparam name="TUnitType">The type of unit enum.</typeparam>
         public void MapUnitToAbbreviation<TUnitType>(TUnitType unit, IFormatProvider? formatProvider, params string[] abbreviations) where TUnitType : struct, Enum
         {
-            PerformAbbreviationMapping(unit, formatProvider, false, abbreviations);
+            PerformAbbreviationMapping(UnitKey.ForUnit(unit), formatProvider, false, abbreviations);
         }
 
         /// <summary>
@@ -132,7 +132,7 @@ public void MapUnitToAbbreviation<TUnitType>(TUnitType unit, IFormatProvider? fo
         /// <typeparam name="TUnitType">The type of unit enum.</typeparam>
         public void MapUnitToDefaultAbbreviation<TUnitType>(TUnitType unit, IFormatProvider? formatProvider, string abbreviation) where TUnitType : struct, Enum
         {
-            PerformAbbreviationMapping(unit, formatProvider, true, abbreviation);
+            PerformAbbreviationMapping(UnitKey.ForUnit(unit), formatProvider, true, abbreviation);
         }
 
         /// <summary>
@@ -146,8 +146,7 @@ public void MapUnitToDefaultAbbreviation<TUnitType>(TUnitType unit, IFormatProvi
         /// <param name="abbreviations">Unit abbreviations to add.</param>
         public void MapUnitToAbbreviation(Type unitType, int unitValue, IFormatProvider? formatProvider, params string[] abbreviations)
         {
-            var enumValue = (Enum)Enum.ToObject(unitType, unitValue);
-            PerformAbbreviationMapping(enumValue, formatProvider, false, abbreviations);
+            PerformAbbreviationMapping(new UnitKey(unitType, unitValue), formatProvider, false, abbreviations);
         }
 
         /// <summary>
@@ -161,15 +160,15 @@ public void MapUnitToAbbreviation(Type unitType, int unitValue, IFormatProvider?
         /// <param name="abbreviation">Unit abbreviation to add as default.</param>
         public void MapUnitToDefaultAbbreviation(Type unitType, int unitValue, IFormatProvider? formatProvider, string abbreviation)
         {
-            var enumValue = (Enum)Enum.ToObject(unitType, unitValue);
-            PerformAbbreviationMapping(enumValue, formatProvider, true, abbreviation);
+            PerformAbbreviationMapping(new UnitKey(unitType, unitValue), formatProvider, true, abbreviation);
         }
 
-        private void PerformAbbreviationMapping(Enum unitValue, IFormatProvider? formatProvider, bool setAsDefault, params string[] abbreviations)
+        private void PerformAbbreviationMapping(UnitKey unitValue, IFormatProvider? formatProvider, bool setAsDefault, params string[] abbreviations)
         {
             if(!QuantityInfoLookup.TryGetUnitInfo(unitValue, out UnitInfo? unitInfo))
             {
-                unitInfo = new UnitInfo(unitValue, unitValue.ToString(), BaseUnits.Undefined);
+                // TODO we should throw QuantityNotFoundException here (all QuantityInfos should be provided through the constructor)
+                unitInfo = new UnitInfo((Enum)unitValue, unitValue.ToString(), BaseUnits.Undefined);
                 QuantityInfoLookup.AddUnitInfo(unitInfo);
             }
 
@@ -280,6 +279,7 @@ public IReadOnlyList<string> GetAllUnitAbbreviationsForQuantity(Type unitEnumTyp
             var allAbbreviations = new List<string>();
             if (!QuantityInfoLookup.TryGetQuantityByUnitType(unitEnumType, out QuantityInfo? quantityInfo))
             {
+                // TODO I think we should either return empty or throw QuantityNotFoundException here
                 var enumValues = Enum.GetValues(unitEnumType).Cast<Enum>();
                 var all = GetStringUnitPairs(enumValues, formatProvider);
                 return all.Select(pair => pair.Item2).ToList();
@@ -351,10 +351,11 @@ public IReadOnlyList<string> GetAbbreviations(UnitInfo unitInfo, IFormatProvider
         private void AddAbbreviation(UnitInfo unitInfo, IFormatProvider? formatProvider, bool setAsDefault,
             params string[] newAbbreviations)
         {
-            if (formatProvider is not CultureInfo)
-                formatProvider = CultureInfo.CurrentCulture;
+            if (formatProvider is not CultureInfo culture)
+            {
+                culture = CultureInfo.CurrentCulture;
+            }
 
-            var culture = (CultureInfo)formatProvider;
             var cultureName = GetCultureNameOrEnglish(culture);
 
             AbbreviationMapKey key = GetAbbreviationMapKey(unitInfo, cultureName);
diff --git a/UnitsNet/CustomCode/UnitsNetSetup.cs b/UnitsNet/CustomCode/UnitsNetSetup.cs
index aa6f22f9f0..3a5abef319 100644
--- a/UnitsNet/CustomCode/UnitsNetSetup.cs
+++ b/UnitsNet/CustomCode/UnitsNetSetup.cs
@@ -2,6 +2,7 @@
 // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
 
 using System.Collections.Generic;
+using System.Linq;
 using UnitsNet.Units;
 
 namespace UnitsNet;
@@ -20,7 +21,7 @@ public sealed class UnitsNetSetup
     static UnitsNetSetup()
     {
         var unitConverter = UnitConverter.CreateDefault();
-        ICollection<QuantityInfo> quantityInfos = Quantity.ByName.Values;
+        IReadOnlyCollection<QuantityInfo> quantityInfos = Quantity.ByName.Values.ToList();
 
         Default = new UnitsNetSetup(quantityInfos, unitConverter);
     }
@@ -30,7 +31,7 @@ static UnitsNetSetup()
     /// </summary>
     /// <param name="quantityInfos">The quantities and their units to support for unit conversions, Parse() and ToString().</param>
     /// <param name="unitConverter">The unit converter instance.</param>
-    public UnitsNetSetup(ICollection<QuantityInfo> quantityInfos, UnitConverter unitConverter)
+    public UnitsNetSetup(IReadOnlyCollection<QuantityInfo> quantityInfos, UnitConverter unitConverter)
     {
         var quantityInfoLookup = new QuantityInfoLookup(quantityInfos);
         var unitAbbreviations = new UnitAbbreviationsCache(quantityInfoLookup);
diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs
index ce30cb2416..73275812cd 100644
--- a/UnitsNet/QuantityInfoLookup.cs
+++ b/UnitsNet/QuantityInfoLookup.cs
@@ -29,7 +29,7 @@ internal class QuantityInfoLookup
     ///     Initializes a new instance of the <see cref="QuantityInfoLookup" /> class.
     /// </summary>
     /// <param name="quantityInfos">A collection of quantity information objects.</param>
-    public QuantityInfoLookup(ICollection<QuantityInfo> quantityInfos)
+    public QuantityInfoLookup(IReadOnlyCollection<QuantityInfo> quantityInfos)
     {
         _quantitiesByName = new Lazy<SortedDictionary<string, QuantityInfo>>(() =>
             new SortedDictionary<string, QuantityInfo>(quantityInfos.ToDictionary(info => info.Name), StringComparer.OrdinalIgnoreCase));

From 6b4d05ae409f2a5e3f1f8808a59b4fa9e9cbf926 Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 7 Feb 2025 14:27:38 +0200
Subject: [PATCH 3/6] - added explicit Equals / HashCode implementations for
 the UnitKey (using backing fields) - added some benchmarks for the
 Enum/UnitKey - updated some typos for the TryParseUnitBenchmarks and
 introduced an explict Culture for the Parse/TryParse unit benchmarks

---
 .../FromString/ParseUnitBenchmarks.cs         |  27 +++--
 .../TryParseInvalidUnitBenchmarks.cs          |  30 +++--
 .../Enums/BoxedEnumToIntegerBenchmarks.cs     |  59 ++++++++++
 .../Enums/EnumToIntegerBenchmarks.cs          |  55 +++++++++
 .../Enums/UnitKeyEqualsBenchmarks.cs          |  60 ++++++++++
 .../Enums/UnitKeyHashCodeBenchmarks.cs        |  72 ++++++++++++
 .../Enums/UnitKeyToEnumBenchmarks.cs          |  82 ++++++++++++++
 UnitsNet.Tests/UnitKeyTest.cs                 |   8 --
 UnitsNet/CustomCode/UnitKey.cs                | 104 ++++++++++++++++--
 9 files changed, 457 insertions(+), 40 deletions(-)
 create mode 100644 UnitsNet.Benchmark/Enums/BoxedEnumToIntegerBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Enums/EnumToIntegerBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Enums/UnitKeyEqualsBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Enums/UnitKeyHashCodeBenchmarks.cs
 create mode 100644 UnitsNet.Benchmark/Enums/UnitKeyToEnumBenchmarks.cs

diff --git a/UnitsNet.Benchmark/Conversions/FromString/ParseUnitBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/ParseUnitBenchmarks.cs
index cf4ab78ec4..5cd182185c 100644
--- a/UnitsNet.Benchmark/Conversions/FromString/ParseUnitBenchmarks.cs
+++ b/UnitsNet.Benchmark/Conversions/FromString/ParseUnitBenchmarks.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Globalization;
 using BenchmarkDotNet.Attributes;
 using BenchmarkDotNet.Jobs;
 using UnitsNet.Units;
@@ -10,6 +11,9 @@ namespace UnitsNet.Benchmark.Conversions.FromString;
 [SimpleJob(RuntimeMoniker.Net80)]
 public class ParseUnitBenchmarks
 {
+    private const int NbAbbreviations = 1000;
+    
+    private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
     private readonly Random _random = new(42);
     private string[] _densityUnits;
     private string[] _massUnits;
@@ -17,37 +21,44 @@ public class ParseUnitBenchmarks
     private string[] _volumeFlowUnits;
     private string[] _volumeUnits = [];
 
-    [Params(1000)]
-    public int NbAbbreviations { get; set; }
-
     [GlobalSetup(Target = nameof(ParseMassUnit))]
     public void PrepareMassUnits()
     {
         _massUnits = _random.GetItems(["mg", "g", "kg", "lbs", "Mlbs"], NbAbbreviations);
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        Mass.TryParseUnit("_invalid", Culture, out _);
     }
 
     [GlobalSetup(Target = nameof(ParseVolumeUnit))]
     public void PrepareVolumeUnits()
     {
         _volumeUnits = _random.GetItems(["ml", "l", "L", "cm³", "m³"], NbAbbreviations);
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        Volume.TryParseUnit("_invalid", Culture, out _);
     }
 
     [GlobalSetup(Target = nameof(ParseDensityUnit))]
     public void PrepareDensityUnits()
     {
         _densityUnits = _random.GetRandomAbbreviations<DensityUnit>(UnitsNetSetup.Default.UnitAbbreviations, NbAbbreviations);
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        Density.TryParseUnit("_invalid", Culture, out _);
     }
 
     [GlobalSetup(Target = nameof(ParsePressureUnit))]
     public void PreparePressureUnits()
     {
         _pressureUnits = _random.GetRandomAbbreviations<PressureUnit>(UnitsNetSetup.Default.UnitAbbreviations, NbAbbreviations);
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        Pressure.TryParseUnit("_invalid", Culture, out _);
     }
 
     [GlobalSetup(Target = nameof(ParseVolumeFlowUnit))]
     public void PrepareVolumeFlowUnits()
     {
         _volumeFlowUnits = _random.GetRandomAbbreviations<VolumeFlowUnit>(UnitsNetSetup.Default.UnitAbbreviations, NbAbbreviations);
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        VolumeFlow.TryParseUnit("_invalid", Culture, out _);
     }
 
     [Benchmark(Baseline = true)]
@@ -56,7 +67,7 @@ public MassUnit ParseMassUnit()
         MassUnit unit = default;
         foreach (var unitToParse in _massUnits)
         {
-            unit = Mass.ParseUnit(unitToParse);
+            unit = Mass.ParseUnit(unitToParse, Culture);
         }
 
         return unit;
@@ -68,7 +79,7 @@ public VolumeUnit ParseVolumeUnit()
         VolumeUnit unit = default;
         foreach (var unitToParse in _volumeUnits)
         {
-            unit = Volume.ParseUnit(unitToParse);
+            unit = Volume.ParseUnit(unitToParse, Culture);
         }
 
         return unit;
@@ -80,7 +91,7 @@ public DensityUnit ParseDensityUnit()
         DensityUnit unit = default;
         foreach (var unitToParse in _densityUnits)
         {
-            unit = Density.ParseUnit(unitToParse);
+            unit = Density.ParseUnit(unitToParse, Culture);
         }
 
         return unit;
@@ -92,7 +103,7 @@ public PressureUnit ParsePressureUnit()
         PressureUnit unit = default;
         foreach (var unitToParse in _pressureUnits)
         {
-            unit = Pressure.ParseUnit(unitToParse);
+            unit = Pressure.ParseUnit(unitToParse, Culture);
         }
 
         return unit;
@@ -104,7 +115,7 @@ public VolumeFlowUnit ParseVolumeFlowUnit()
         VolumeFlowUnit unit = default;
         foreach (var unitToParse in _volumeFlowUnits)
         {
-            unit = VolumeFlow.ParseUnit(unitToParse);
+            unit = VolumeFlow.ParseUnit(unitToParse, Culture);
         }
 
         return unit;
diff --git a/UnitsNet.Benchmark/Conversions/FromString/TryParseInvalidUnitBenchmarks.cs b/UnitsNet.Benchmark/Conversions/FromString/TryParseInvalidUnitBenchmarks.cs
index 13515c63a7..bd24c1c579 100644
--- a/UnitsNet.Benchmark/Conversions/FromString/TryParseInvalidUnitBenchmarks.cs
+++ b/UnitsNet.Benchmark/Conversions/FromString/TryParseInvalidUnitBenchmarks.cs
@@ -2,11 +2,11 @@
 // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
 
 using System;
+using System.Globalization;
 using System.Linq;
 using System.Text;
 using BenchmarkDotNet.Attributes;
 using BenchmarkDotNet.Jobs;
-using UnitsNet.Units;
 
 namespace UnitsNet.Benchmark.Conversions.FromString;
 
@@ -15,16 +15,22 @@ namespace UnitsNet.Benchmark.Conversions.FromString;
 [SimpleJob(RuntimeMoniker.Net80)]
 public class TryParseInvalidUnitBenchmarks
 {
+    private const int NbAbbreviations = 1000;
+
+    private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
     private readonly Random _random = new(42);
     private string[] _invalidUnits = [];
 
-    [Params(1000)]
-    public int NbAbbreviations { get; set; }
-
     [GlobalSetup]
     public void Setup()
     {
         _invalidUnits = Enumerable.Range(0, NbAbbreviations).Select(_ => GenerateInvalidUnit()).ToArray();
+        // initializes the QuantityInfoLookup and the abbreviations cache
+        Mass.TryParseUnit("_invalid", Culture, out _);
+        Volume.TryParseUnit("_invalid", Culture, out _);
+        Density.TryParseUnit("_invalid", Culture, out _);
+        Pressure.TryParseUnit("_invalid", Culture, out _);
+        VolumeFlow.TryParseUnit("_invalid", Culture, out _);
     }
 
     private string GenerateInvalidUnit()
@@ -46,7 +52,7 @@ public bool TryParseMassUnit()
         var success = true;
         foreach (var unitToParse in _invalidUnits)
         {
-            success = Mass.TryParseUnit(unitToParse, out MassUnit _);
+            success = Mass.TryParseUnit(unitToParse, Culture, out _);
         }
 
         return success;
@@ -58,43 +64,43 @@ public bool TryParseVolumeUnit()
         var success = true;
         foreach (var unitToParse in _invalidUnits)
         {
-            success = Volume.TryParseUnit(unitToParse, out _);
+            success = Volume.TryParseUnit(unitToParse, Culture, out _);
         }
 
         return success;
     }
 
     [Benchmark(Baseline = false)]
-    public bool ParseDensityUnit()
+    public bool TryParseDensityUnit()
     {
         var success = true;
         foreach (var unitToParse in _invalidUnits)
         {
-            success = Density.TryParseUnit(unitToParse, out _);
+            success = Density.TryParseUnit(unitToParse, Culture, out _);
         }
 
         return success;
     }
 
     [Benchmark(Baseline = false)]
-    public bool ParsePressureUnit()
+    public bool TryParsePressureUnit()
     {
         var success = true;
         foreach (var unitToParse in _invalidUnits)
         {
-            success = Pressure.TryParseUnit(unitToParse, out _);
+            success = Pressure.TryParseUnit(unitToParse, Culture, out _);
         }
 
         return success;
     }
 
     [Benchmark(Baseline = false)]
-    public bool ParseVolumeFlowUnit()
+    public bool TryParseVolumeFlowUnit()
     {
         var success = true;
         foreach (var unitToParse in _invalidUnits)
         {
-            success = VolumeFlow.TryParseUnit(unitToParse, out _);
+            success = VolumeFlow.TryParseUnit(unitToParse, Culture, out _);
         }
 
         return success;
diff --git a/UnitsNet.Benchmark/Enums/BoxedEnumToIntegerBenchmarks.cs b/UnitsNet.Benchmark/Enums/BoxedEnumToIntegerBenchmarks.cs
new file mode 100644
index 0000000000..18cd6f3432
--- /dev/null
+++ b/UnitsNet.Benchmark/Enums/BoxedEnumToIntegerBenchmarks.cs
@@ -0,0 +1,59 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Enums;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class BoxedEnumToIntegerBenchmarks
+{
+    private const int NbIterations = 1000;
+
+    private static readonly Enum Unit = MassUnit.Gram;
+
+    [Benchmark(Baseline = true)]
+    public int ConvertToInt32()
+    {
+        Enum unit = Unit;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += Convert.ToInt32(unit);
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ConvertWithCast()
+    {
+        Enum unit = Unit;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += (int)(object)unit;
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ConvertWithUnsafe()
+    {
+        Enum unit = Unit;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += Unsafe.Unbox<int>(unit);
+        }
+
+        return total;
+    }
+}
diff --git a/UnitsNet.Benchmark/Enums/EnumToIntegerBenchmarks.cs b/UnitsNet.Benchmark/Enums/EnumToIntegerBenchmarks.cs
new file mode 100644
index 0000000000..75fc0b2fc3
--- /dev/null
+++ b/UnitsNet.Benchmark/Enums/EnumToIntegerBenchmarks.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Enums;
+
+[ShortRunJob(RuntimeMoniker.Net48)]
+[ShortRunJob(RuntimeMoniker.Net80)]
+public class EnumToIntegerBenchmarks
+{
+    private const int NbIterations = 1000;
+
+    private const MassUnit Unit = MassUnit.Gram;
+
+    [Benchmark(Baseline = true)]
+    public int ConvertToInt32()
+    {
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += Convert.ToInt32(Unit);
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ConvertWithCast()
+    {
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += (int)Unit;
+        }
+
+        return total;
+    }
+
+    // #if NET
+    [Benchmark(Baseline = false)]
+    public int ConvertWithUnsafe()
+    {
+        MassUnit unit = Unit;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            total += Unsafe.As<MassUnit, int>(ref unit);
+        }
+
+        return total;
+    }
+    // #endif
+}
diff --git a/UnitsNet.Benchmark/Enums/UnitKeyEqualsBenchmarks.cs b/UnitsNet.Benchmark/Enums/UnitKeyEqualsBenchmarks.cs
new file mode 100644
index 0000000000..e763b8233a
--- /dev/null
+++ b/UnitsNet.Benchmark/Enums/UnitKeyEqualsBenchmarks.cs
@@ -0,0 +1,60 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Enums;
+
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class UnitKeyEqualsBenchmarks
+{
+    private const int NbIterations = 1000;
+
+    private static readonly UnitKey UnitKey = UnitKey.ForUnit(VolumeUnit.CubicMeter);
+    private static readonly UnitKey OtherUnitKey = UnitKey.ForUnit(VolumeUnit.AcreFoot);
+    private readonly Type OtherUnitType = UnitKey.UnitType;
+    private readonly int OtherUnitValue = UnitKey.UnitValue;
+
+    private readonly Type UnitType = UnitKey.UnitType;
+    private readonly int UnitValue = UnitKey.UnitValue;
+
+    [Benchmark(Baseline = true)]
+    public bool EqualsRecord()
+    {
+        bool equal = false;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            equal = UnitKey.Equals(OtherUnitKey);
+        }
+
+        return equal;
+    }
+
+    [Benchmark(Baseline = false)]
+    public bool OperatorEqualsRecord()
+    {
+        bool equal = false;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            equal = UnitKey == OtherUnitKey;
+        }
+        
+        return equal;
+    }
+
+    [Benchmark]
+    public bool OperatorEqualsManual()
+    {
+        bool equal = false;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            equal = UnitType == OtherUnitType && UnitValue == OtherUnitValue;
+        }
+        
+        return equal;
+    }
+}
diff --git a/UnitsNet.Benchmark/Enums/UnitKeyHashCodeBenchmarks.cs b/UnitsNet.Benchmark/Enums/UnitKeyHashCodeBenchmarks.cs
new file mode 100644
index 0000000000..cdc09972c3
--- /dev/null
+++ b/UnitsNet.Benchmark/Enums/UnitKeyHashCodeBenchmarks.cs
@@ -0,0 +1,72 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Enums;
+
+// [MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class UnitKeyHashCodeBenchmarks
+{
+    private const int NbIterations = 1000;
+
+    private static readonly UnitKey UnitKey = UnitKey.ForUnit(VolumeUnit.CubicMeter);
+
+    private readonly Type UnitType = UnitKey.UnitType;
+    private readonly int UnitValue = UnitKey.UnitValue;
+
+    [Benchmark(Baseline = true)]
+    public int GetHashCodeRecord()
+    {
+        int hashCode = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            hashCode += UnitKey.GetHashCode();
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    public int GetCustomHashCode()
+    {
+        int hashCode = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+#if NET
+            hashCode += HashCode.Combine(UnitType, UnitValue);
+#else
+            hashCode += (UnitType.GetHashCode() * 397) ^ UnitValue;
+#endif
+        }
+        
+        return hashCode;
+    }
+
+    [Benchmark]
+    public int GetCustomHashCodeUnchecked()
+    {
+        int hashCode = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            if (UnitType == null)
+            {
+                hashCode += UnitValue;
+            }
+            else
+            {
+                unchecked
+                {
+                    hashCode += (UnitType.GetHashCode() * 397) ^ UnitValue;
+                }
+            }
+        }
+
+        return hashCode;
+    }
+}
diff --git a/UnitsNet.Benchmark/Enums/UnitKeyToEnumBenchmarks.cs b/UnitsNet.Benchmark/Enums/UnitKeyToEnumBenchmarks.cs
new file mode 100644
index 0000000000..d70e9db227
--- /dev/null
+++ b/UnitsNet.Benchmark/Enums/UnitKeyToEnumBenchmarks.cs
@@ -0,0 +1,82 @@
+// Licensed under MIT No Attribution, see LICENSE file at the root.
+// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
+
+using System;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using UnitsNet.Units;
+
+namespace UnitsNet.Benchmark.Enums;
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net80)]
+public class UnitKeyToEnumBenchmarks
+{
+    private const int NbIterations = 500;
+    private static readonly UnitKey UnitKey = MassUnit.Gram;
+
+    [Benchmark(Baseline = true)]
+    public int ManualCast()
+    {
+        UnitKey unitKey = UnitKey;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            if ((MassUnit)unitKey.UnitValue == MassUnit.Gram)
+            {
+                total++;
+            }
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ExplicitCast()
+    {
+        UnitKey unitKey = UnitKey;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            if ((MassUnit)unitKey == MassUnit.Gram)
+            {
+                total++;
+            }
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ExplicitCastBoxed()
+    {
+        UnitKey unitKey = UnitKey;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            if (MassUnit.Gram.Equals((Enum)unitKey))
+            {
+                total++;
+            }
+        }
+
+        return total;
+    }
+
+    [Benchmark(Baseline = false)]
+    public int ToUnit()
+    {
+        UnitKey unitKey = UnitKey;
+        var total = 0;
+        for (var i = 0; i < NbIterations; i++)
+        {
+            if (unitKey.ToUnit<MassUnit>() == MassUnit.Gram)
+            {
+                total++;
+            }
+        }
+
+        return total;
+    }
+}
diff --git a/UnitsNet.Tests/UnitKeyTest.cs b/UnitsNet.Tests/UnitKeyTest.cs
index a064d35356..215a3ac7ba 100644
--- a/UnitsNet.Tests/UnitKeyTest.cs
+++ b/UnitsNet.Tests/UnitKeyTest.cs
@@ -35,14 +35,6 @@ public void Constructor_WithNullType_ShouldNotThrow()
         Assert.Equal(0, unitKey.UnitValue);
     }
 
-    [Fact]
-    public void ConstructingWithInitializer_ShouldAssignFields()
-    {
-        var unitKey = new UnitKey { UnitType = typeof(TestUnit), UnitValue = 42 };
-        Assert.Equal(typeof(TestUnit), unitKey.UnitType);
-        Assert.Equal(42, unitKey.UnitValue);
-    }
-
     [Theory]
     [InlineData(TestUnit.Unit1)]
     [InlineData(TestUnit.Unit2)]
diff --git a/UnitsNet/CustomCode/UnitKey.cs b/UnitsNet/CustomCode/UnitKey.cs
index bedcde65c9..f759dac2a0 100644
--- a/UnitsNet/CustomCode/UnitKey.cs
+++ b/UnitsNet/CustomCode/UnitKey.cs
@@ -2,6 +2,8 @@
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
 
+// ReSharper disable ConvertToAutoPropertyWhenPossible
+
 namespace UnitsNet;
 
 /// <summary>
@@ -12,12 +14,49 @@ namespace UnitsNet;
 ///     as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
 /// </remarks>
 [DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
-#if NET
-public readonly record struct UnitKey(Type UnitType, int UnitValue)
-#else
-public record struct UnitKey(Type UnitType, int UnitValue)
-#endif
+public readonly record struct UnitKey
 {
+    // apparently, on netstandard, the use of auto-properties is significantly slower
+    private readonly Type _unitType;
+    private readonly int _unitValue;
+
+    /// <summary>
+    ///     Represents a unique key for a unit type and its corresponding value.
+    /// </summary>
+    /// <remarks>
+    ///     This key is particularly useful when using an enum-based unit in a hash-based collection,
+    ///     as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
+    /// </remarks>
+    public UnitKey(Type UnitType, int UnitValue)
+    {
+        _unitType = UnitType;
+        _unitValue = UnitValue;
+    }
+
+    /// <summary>
+    ///     Gets the type of the unit represented by this <see cref="UnitKey" />.
+    /// </summary>
+    /// <remarks>
+    ///     This property holds the <see cref="Type" /> of the unit enumeration associated with this key.
+    ///     It is particularly useful for identifying the unit type in scenarios where multiple unit types are used.
+    /// </remarks>
+    public Type UnitType
+    {
+        get => _unitType;
+    }
+
+    /// <summary>
+    ///     Gets the integer value associated with the unit type.
+    /// </summary>
+    /// <remarks>
+    ///     This property represents the unique value of the unit within its type, typically corresponding to the underlying
+    ///     integer value of an enumeration.
+    /// </remarks>
+    public int UnitValue
+    {
+        get => _unitValue;
+    }
+
     /// <summary>
     ///     Creates a new instance of the <see cref="UnitKey" /> struct for the specified unit.
     /// </summary>
@@ -72,7 +111,7 @@ public static implicit operator UnitKey(Enum unit)
     /// </remarks>
     public static explicit operator Enum(UnitKey unitKey)
     {
-        return (Enum)Enum.ToObject(unitKey.UnitType, unitKey.UnitValue);
+        return (Enum)Enum.ToObject(unitKey._unitType, unitKey._unitValue);
     }
 
     /// <summary>
@@ -90,12 +129,12 @@ public static explicit operator Enum(UnitKey unitKey)
     /// </remarks>
     public TUnit ToUnit<TUnit>() where TUnit : struct, Enum
     {
-        if (typeof(TUnit) != UnitType)
+        if (typeof(TUnit) != _unitType)
         {
-            throw new InvalidOperationException($"Cannot convert UnitKey of type {UnitType} to {typeof(TUnit)}.");
+            throw new InvalidOperationException($"Cannot convert UnitKey of type {_unitType} to {typeof(TUnit)}.");
         }
 
-        var unitValue = UnitValue;
+        var unitValue = _unitValue;
         return Unsafe.As<int, TUnit>(ref unitValue);
     }
 
@@ -103,12 +142,53 @@ private string GetDebuggerDisplay()
     {
         try
         {
-            var unitName = Enum.GetName(UnitType, UnitValue);
-            return string.IsNullOrEmpty(unitName) ? $"{nameof(UnitType)}: {UnitType}, {nameof(UnitValue)} = {UnitValue}" : $"{UnitType.Name}.{unitName}";
+            var unitName = Enum.GetName(_unitType, _unitValue);
+            return string.IsNullOrEmpty(unitName) ? $"{nameof(UnitType)}: {_unitType}, {nameof(UnitValue)} = {_unitValue}" : $"{_unitType.Name}.{unitName}";
         }
         catch
         {
-            return $"{nameof(UnitType)}: {UnitType}, {nameof(UnitValue)} = {UnitValue}";
+            return $"{nameof(UnitType)}: {_unitType}, {nameof(UnitValue)} = {_unitValue}";
         }
     }
+
+    /// <summary>
+    ///     Deconstructs the <see cref="UnitKey" /> into its component parts.
+    /// </summary>
+    /// <param name="unitType">The type of the unit.</param>
+    /// <param name="unitValue">The value of the unit.</param>
+    /// <remarks>
+    ///     This method allows for the use of deconstruction syntax to extract the unit type and value
+    ///     from a <see cref="UnitKey" /> instance.
+    /// </remarks>
+    public void Deconstruct(out Type unitType, out int unitValue)
+    {
+        unitType = _unitType;
+        unitValue = _unitValue;
+    }
+
+    #region Equality members
+
+    /// <inheritdoc />
+    public bool Equals(UnitKey other)
+    {
+        // implementing the Equality members on net48 is 5x faster than the default
+        return _unitType == other._unitType && _unitValue == other._unitValue;
+    }
+
+    /// <inheritdoc />
+    public override int GetHashCode()
+    {
+        // implementing the Equality members on net48 is 5x faster than the default
+        if (_unitType == null)
+        {
+            return _unitValue;
+        }
+
+        unchecked
+        {
+            return (_unitType.GetHashCode() * 397) ^ _unitValue;
+        }
+    }
+
+    #endregion
 }

From 19c211af758945100dd77c1933716639d53861d0 Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 7 Feb 2025 15:00:27 +0200
Subject: [PATCH 4/6] adding the (now missing) deconstructor test

---
 UnitsNet.Tests/UnitKeyTest.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/UnitsNet.Tests/UnitKeyTest.cs b/UnitsNet.Tests/UnitKeyTest.cs
index 215a3ac7ba..28b78eeb47 100644
--- a/UnitsNet.Tests/UnitKeyTest.cs
+++ b/UnitsNet.Tests/UnitKeyTest.cs
@@ -127,6 +127,14 @@ public void DefaultToUnit_ShouldThrowInvalidOperationExceptionForMismatchedType(
         Assert.Throws<InvalidOperationException>(() => defaultUnitKey.ToUnit<TestUnit>());
     }
 
+    [Fact]
+    public void Deconstruct_ShouldReturnTheUnitTypeAndUnitValue()
+    {
+        (Type unitType, var unitValue) = UnitKey.ForUnit(TestUnit.Unit1);
+        Assert.Equal(typeof(TestUnit), unitType);
+        Assert.Equal(1, unitValue);
+    }
+
     [Theory]
     [InlineData(TestUnit.Unit1, "TestUnit.Unit1")]
     [InlineData(TestUnit.Unit2, "TestUnit.Unit2")]

From 8e2f8974ee5f59fb28edcf055a2491f0f5aae36d Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 7 Feb 2025 16:25:40 +0200
Subject: [PATCH 5/6] QuantityInfoLookup: removed the (still) unused 
 _quantitiesByType dictionary and optimized the lazy initializer

---
 UnitsNet/QuantityInfoLookup.cs | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs
index 73275812cd..1bf145159a 100644
--- a/UnitsNet/QuantityInfoLookup.cs
+++ b/UnitsNet/QuantityInfoLookup.cs
@@ -21,7 +21,6 @@ namespace UnitsNet;
 internal class QuantityInfoLookup
 {
     private readonly Lazy<SortedDictionary<string, QuantityInfo>> _quantitiesByName;
-    private readonly Lazy<QuantityByTypeLookupDictionary> _quantitiesByType;
     private readonly Lazy<QuantityByTypeLookupDictionary> _quantitiesByUnitType;
     private readonly Lazy<UnitByKeyLookupDictionary> _unitsByKey;
 
@@ -32,13 +31,19 @@ internal class QuantityInfoLookup
     public QuantityInfoLookup(IReadOnlyCollection<QuantityInfo> quantityInfos)
     {
         _quantitiesByName = new Lazy<SortedDictionary<string, QuantityInfo>>(() =>
-            new SortedDictionary<string, QuantityInfo>(quantityInfos.ToDictionary(info => info.Name), StringComparer.OrdinalIgnoreCase));
+        {
+            var sortedDictionary = new SortedDictionary<string, QuantityInfo>(StringComparer.OrdinalIgnoreCase);
+            foreach (QuantityInfo quantityInfo in quantityInfos)
+            {
+                sortedDictionary.Add(quantityInfo.Name, quantityInfo);
+            }
+
+            return sortedDictionary;
+        });
 
 #if NET8_0_OR_GREATER
-        _quantitiesByType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToFrozenDictionary(info => info.QuantityType));
         _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToFrozenDictionary(info => info.UnitType));
 #else
-        _quantitiesByType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToDictionary(info => info.QuantityType));
         _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToDictionary(info => info.UnitType));
 #endif
         _unitsByKey = new Lazy<UnitByKeyLookupDictionary>(() => quantityInfos.SelectMany(quantityInfo => quantityInfo.UnitInfos).ToDictionary(x => x.UnitKey));

From 6dc5f0979e1acd6839fde559fd35a9ca0805338f Mon Sep 17 00:00:00 2001
From: lipchev <lipchev@gmail.com>
Date: Fri, 7 Feb 2025 18:53:51 +0200
Subject: [PATCH 6/6] - QuantityInfoLookup: introduced an array of QuantityInfo
 and replaced the _quantitiesByName into a regular (lazy-loaded) dictionary -
 UnitsNetSetup and UnitAbbreviationsCache constructors changed from
 IReadOnlyCollection<QuantityInfo> into IEnumerable<QuantityInfo> - changed
 the Quantity.Infos from IReadOnlyCollection to IReadOnlyList

---
 UnitsNet/CustomCode/Quantity.cs               |  6 +-
 UnitsNet/CustomCode/UnitAbbreviationsCache.cs |  2 +-
 UnitsNet/CustomCode/UnitsNetSetup.cs          |  2 +-
 UnitsNet/QuantityInfoLookup.cs                | 62 +++++++++++++------
 4 files changed, 47 insertions(+), 25 deletions(-)

diff --git a/UnitsNet/CustomCode/Quantity.cs b/UnitsNet/CustomCode/Quantity.cs
index 19d98b8f4f..d606ea55da 100644
--- a/UnitsNet/CustomCode/Quantity.cs
+++ b/UnitsNet/CustomCode/Quantity.cs
@@ -12,14 +12,14 @@ public partial class Quantity
         private static UnitParser UnitParser => UnitsNetSetup.Default.UnitParser;
 
         /// <summary>
-        /// All enum value names of <see cref="Infos"/>, such as "Length" and "Mass".
+        /// All quantity names of <see cref="Infos"/>, such as "Length" and "Mass".
         /// </summary>
-        public static IReadOnlyCollection<string> Names { get => Quantities.Names; }
+        public static IReadOnlyCollection<string> Names => Quantities.Names;
 
         /// <summary>
         /// All quantity information objects, such as <see cref="Length.Info"/> and <see cref="Mass.Info"/>.
         /// </summary>
-        public static IReadOnlyCollection<QuantityInfo> Infos => Quantities.Infos;
+        public static IReadOnlyList<QuantityInfo> Infos => Quantities.Infos;
 
         /// <summary>
         /// Get <see cref="UnitInfo"/> for a given unit enum value.
diff --git a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
index 0c59494db4..bcfea9a4fe 100644
--- a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
+++ b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
@@ -59,7 +59,7 @@ public UnitAbbreviationsCache()
         ///     Creates an instance of the cache using the specified set of quantities.
         /// </summary>
         /// <returns>Instance for mapping the units of the provided quantities.</returns>
-        public UnitAbbreviationsCache(IReadOnlyCollection<QuantityInfo> quantities)
+        public UnitAbbreviationsCache(IEnumerable<QuantityInfo> quantities)
             :this(new QuantityInfoLookup(quantities))
         {
         }
diff --git a/UnitsNet/CustomCode/UnitsNetSetup.cs b/UnitsNet/CustomCode/UnitsNetSetup.cs
index 3a5abef319..76ac4c7d26 100644
--- a/UnitsNet/CustomCode/UnitsNetSetup.cs
+++ b/UnitsNet/CustomCode/UnitsNetSetup.cs
@@ -31,7 +31,7 @@ static UnitsNetSetup()
     /// </summary>
     /// <param name="quantityInfos">The quantities and their units to support for unit conversions, Parse() and ToString().</param>
     /// <param name="unitConverter">The unit converter instance.</param>
-    public UnitsNetSetup(IReadOnlyCollection<QuantityInfo> quantityInfos, UnitConverter unitConverter)
+    public UnitsNetSetup(IEnumerable<QuantityInfo> quantityInfos, UnitConverter unitConverter)
     {
         var quantityInfoLookup = new QuantityInfoLookup(quantityInfos);
         var unitAbbreviations = new UnitAbbreviationsCache(quantityInfoLookup);
diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs
index 1bf145159a..2c3dc302ad 100644
--- a/UnitsNet/QuantityInfoLookup.cs
+++ b/UnitsNet/QuantityInfoLookup.cs
@@ -5,8 +5,10 @@
 #if NET8_0_OR_GREATER
 using System.Collections.Frozen;
 using QuantityByTypeLookupDictionary = System.Collections.Frozen.FrozenDictionary<System.Type, UnitsNet.QuantityInfo>;
+using QuantityByNameLookupDictionary = System.Collections.Frozen.FrozenDictionary<string, UnitsNet.QuantityInfo>;
 #else
 using QuantityByTypeLookupDictionary = System.Collections.Generic.Dictionary<System.Type, UnitsNet.QuantityInfo>;
+using QuantityByNameLookupDictionary = System.Collections.Generic.Dictionary<string, UnitsNet.QuantityInfo>;
 #endif
 using UnitByKeyLookupDictionary = System.Collections.Generic.Dictionary<UnitsNet.UnitKey, UnitsNet.UnitInfo>;
 
@@ -20,33 +22,53 @@ namespace UnitsNet;
 /// </remarks>
 internal class QuantityInfoLookup
 {
-    private readonly Lazy<SortedDictionary<string, QuantityInfo>> _quantitiesByName;
+    private readonly QuantityInfo[] _quantities;
+    private readonly Lazy<QuantityByNameLookupDictionary> _quantitiesByName;
     private readonly Lazy<QuantityByTypeLookupDictionary> _quantitiesByUnitType;
     private readonly Lazy<UnitByKeyLookupDictionary> _unitsByKey;
 
-    /// <summary>
-    ///     Initializes a new instance of the <see cref="QuantityInfoLookup" /> class.
-    /// </summary>
-    /// <param name="quantityInfos">A collection of quantity information objects.</param>
-    public QuantityInfoLookup(IReadOnlyCollection<QuantityInfo> quantityInfos)
+    private QuantityByNameLookupDictionary GroupQuantitiesByName()
     {
-        _quantitiesByName = new Lazy<SortedDictionary<string, QuantityInfo>>(() =>
-        {
-            var sortedDictionary = new SortedDictionary<string, QuantityInfo>(StringComparer.OrdinalIgnoreCase);
-            foreach (QuantityInfo quantityInfo in quantityInfos)
-            {
-                sortedDictionary.Add(quantityInfo.Name, quantityInfo);
-            }
-
-            return sortedDictionary;
-        });
+#if NET8_0_OR_GREATER
+        return _quantities.ToFrozenDictionary(info => info.Name, StringComparer.OrdinalIgnoreCase);
+#else
+        return _quantities.ToDictionary(info => info.Name, StringComparer.OrdinalIgnoreCase);
+#endif
+    }
 
+    private QuantityByTypeLookupDictionary GroupQuantitiesByUnitType()
+    {
 #if NET8_0_OR_GREATER
-        _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToFrozenDictionary(info => info.UnitType));
+        return _quantities.ToFrozenDictionary(info => info.UnitType);
 #else
-        _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(() => quantityInfos.ToDictionary(info => info.UnitType));
+        return _quantities.ToDictionary(info => info.UnitType);
 #endif
-        _unitsByKey = new Lazy<UnitByKeyLookupDictionary>(() => quantityInfos.SelectMany(quantityInfo => quantityInfo.UnitInfos).ToDictionary(x => x.UnitKey));
+    }
+
+    private UnitByKeyLookupDictionary GroupUnitsByKey()
+    {
+        return _quantities.SelectMany(quantityInfo => quantityInfo.UnitInfos).ToDictionary(x => x.UnitKey);
+    }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="QuantityInfoLookup" /> class.
+    /// </summary>
+    /// <param name="quantityInfos">
+    ///     A collection of <see cref="QuantityInfo" /> objects representing the quantities to be managed by this lookup.
+    /// </param>
+    /// <exception cref="ArgumentNullException">
+    ///     Thrown when the <paramref name="quantityInfos" /> parameter is <c>null</c>.
+    /// </exception>
+    /// <remarks>
+    ///     This constructor organizes the provided quantity information into internal lookup structures
+    ///     for efficient access by name, unit type, and unit key.
+    /// </remarks>
+    public QuantityInfoLookup(IEnumerable<QuantityInfo> quantityInfos)
+    {
+        _quantities = quantityInfos.ToArray();
+        _quantitiesByName = new Lazy<QuantityByNameLookupDictionary>(GroupQuantitiesByName);
+        _quantitiesByUnitType = new Lazy<QuantityByTypeLookupDictionary>(GroupQuantitiesByUnitType);
+        _unitsByKey = new Lazy<UnitByKeyLookupDictionary>(GroupUnitsByKey);
     }
 
     /// <summary>
@@ -62,7 +84,7 @@ public QuantityInfoLookup(IReadOnlyCollection<QuantityInfo> quantityInfos)
     /// <summary>
     ///     All quantity information objects, such as <see cref="Length.Info" /> and <see cref="Mass.Info" />.
     /// </summary>
-    public IReadOnlyCollection<QuantityInfo> Infos => _quantitiesByName.Value.Values;
+    public IReadOnlyList<QuantityInfo> Infos => _quantities;
 
     /// <summary>
     ///     Retrieves the <see cref="UnitInfo" /> for a specified <see cref="UnitKey" />.