diff --git a/src/Benchmarks/EnumUtilityBenchmarks.cs b/src/Benchmarks/EnumUtilityBenchmarks.cs new file mode 100644 index 0000000000..527acc7653 --- /dev/null +++ b/src/Benchmarks/EnumUtilityBenchmarks.cs @@ -0,0 +1,62 @@ +using BenchmarkDotNet.Attributes; +using Hl7.Fhir.Model; +using Hl7.Fhir.Utility; +using System; + +namespace Firely.Sdk.Benchmarks +{ + [MemoryDiagnoser] + public class EnumUtilityBenchmarks + { + private static readonly SearchParamType StringSearchParam = SearchParamType.String; + private static readonly Enum StringSearchParamEnum = StringSearchParam; + + [Benchmark] + public string EnumToString() + => SearchParamType.String.ToString(); + + [Benchmark] + public string EnumGetName() + => Enum.GetName(StringSearchParam); + + [Benchmark] + public string EnumUtilityGetLiteral() + => EnumUtility.GetLiteral(StringSearchParam); + + [Benchmark] + public string EnumUtilityGetLiteralNonGeneric() + => EnumUtility.GetLiteral(StringSearchParamEnum); + + [Benchmark] + public SearchParamType EnumParse() + => Enum.Parse("String"); + + [Benchmark] + public SearchParamType EnumParseIgnoreCase() + => Enum.Parse("string", true); + + [Benchmark] + public SearchParamType EnumUtilityParseLiteral() + => EnumUtility.ParseLiteral("string").Value; + + [Benchmark] + public Enum EnumUtilityParseLiteralNonGeneric() + => EnumUtility.ParseLiteral("string", typeof(SearchParamType)); + + [Benchmark] + public SearchParamType EnumUtilityParseLiteralIgnoreCase() + => EnumUtility.ParseLiteral("string", true).Value; + + [Benchmark] + public Enum EnumUtilityParseLiteralIgnoreCaseNonGeneric() + => EnumUtility.ParseLiteral("string", typeof(SearchParamType), true); + + [Benchmark] + public string? EnumUtilityGetSystem() + => EnumUtility.GetSystem(StringSearchParam); + + [Benchmark] + public string EnumUtilityGetSystemNonGeneric() + => EnumUtility.GetSystem(StringSearchParamEnum); + } +} diff --git a/src/Hl7.Fhir.Base/Utility/EnumUtility.cs b/src/Hl7.Fhir.Base/Utility/EnumUtility.cs index 0bfeb72d34..b5ba598679 100644 --- a/src/Hl7.Fhir.Base/Utility/EnumUtility.cs +++ b/src/Hl7.Fhir.Base/Utility/EnumUtility.cs @@ -26,11 +26,28 @@ public static class EnumUtility /// public static string GetLiteral(this Enum e) => getEnumMapping(e.GetType()).GetLiteral(e); + /// + /// Retrieves the literal value for the code represented by this enum , or the member name itself if there + /// is no literal value defined. + /// + public static string GetLiteral(this T e) where T : struct, Enum => EnumMappingCache.GetLiteral(e); + + /// + /// Retrieves the literal value for the code represented by this nullable enum , or the member name itself if there + /// is no literal value defined, or null if the enum does not have a value. + /// + public static string? GetLiteral(this T? e) where T : struct, Enum => e.HasValue ? e.Value.GetLiteral() : null; + /// /// Retrieves the system canonical for the code represented by this enum value, or null if there is no system defined. /// public static string? GetSystem(this Enum e) => e.GetAttributeOnEnum()?.System ?? e.GetType().GetCustomAttribute()?.DefaultCodeSystem; + /// + /// Retrieves the system canonical for the code represented by this enum , or null if there is no system defined. + /// + public static string? GetSystem(this T e) where T : struct, Enum => EnumMappingCache.GetSystem(e); + /// /// Retrieves the description for this enum value or the enumeration value itself if there is no description defined. /// @@ -48,8 +65,8 @@ public static string GetDocumentation(this Enum e) => /// /// Finds an enumeration value from enum where the literal is the same as . /// - public static T? ParseLiteral(string? rawValue, bool ignoreCase = false) where T : struct - => (T?)(object?)ParseLiteral(rawValue, typeof(T), ignoreCase); + public static T? ParseLiteral(string? rawValue, bool ignoreCase = false) where T : struct, Enum + => EnumMappingCache.ParseLiteral(rawValue, ignoreCase); /// /// Gets the human readable name defined for the enumeration . @@ -59,11 +76,68 @@ public static string GetDocumentation(this Enum e) => /// /// Gets the human readable name defined for the enumeration . /// - public static string GetName() where T : struct => GetName(typeof(T)); + public static string GetName() where T : struct, Enum => EnumMappingCache.Name; private static EnumMapping getEnumMapping(Type enumType) => CACHE.GetOrAdd(enumType, t => EnumMapping.Create(t)); + private static class EnumMappingCache + where TEnum : struct, Enum + { + static EnumMappingCache() + { + var t = typeof(TEnum); + var enumAttr = t.GetTypeInfo().GetCustomAttribute(); + Name = enumAttr?.BindingName ?? t.Name; + DefaultCodeSystem = enumAttr?.DefaultCodeSystem; + foreach (var enumValue in ReflectionHelper.FindEnumFields(t)) + { + var attr = ReflectionHelper.GetAttribute(enumValue); + string literal = attr?.Literal ?? enumValue.Name; + + var value = (TEnum)enumValue.GetValue(null)!; + + _enumToLiteral.Add(value, literal); + _literalToEnum.Add(literal, value); + _caseInsensitiveLiteralToEnum.Add(literal, value); + if (attr?.System is string systemVal) + { + _enumToSystem.Add(value, systemVal); + } + } + } + + public static string Name { get; } + + public static string? DefaultCodeSystem { get; } + + private static readonly Dictionary _literalToEnum = new(); + private static readonly Dictionary _caseInsensitiveLiteralToEnum = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _enumToLiteral = new(); + private static readonly Dictionary _enumToSystem = new(); + + public static string? GetSystem(TEnum value) => + !_enumToSystem.TryGetValue(value, out string? result) + ? DefaultCodeSystem + : result; + + public static string GetLiteral(TEnum value) => + !_enumToLiteral.TryGetValue(value, out string? result) + ? throw new InvalidOperationException($"Should only pass enum values that are member of the given enum: {value} is not a member of {Name}.") + : result; + + public static TEnum? ParseLiteral(string? literal, bool ignoreCase) + { + if (literal is null) return null; + + var success = ignoreCase + ? _caseInsensitiveLiteralToEnum.TryGetValue(literal, out TEnum result) + : _literalToEnum.TryGetValue(literal, out result); + + return success ? result : null; + } + } + internal class EnumMapping { internal EnumMapping(string name, Type enumType) @@ -79,7 +153,7 @@ internal EnumMapping(string name, Type enumType) public Type EnumType { get; private set; } private readonly Dictionary _literalToEnum = new(); - private readonly Dictionary _lowercaseLiteralToEnum = new(); + private readonly Dictionary _caseInsensitiveLiteralToEnum = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _enumToLiteral = new(); public string GetLiteral(Enum value) => @@ -92,7 +166,7 @@ public string GetLiteral(Enum value) => if (literal is null) return null; var success = ignoreCase - ? _lowercaseLiteralToEnum.TryGetValue(literal.ToLowerInvariant(), out Enum? result) + ? _caseInsensitiveLiteralToEnum.TryGetValue(literal, out Enum? result) : _literalToEnum.TryGetValue(literal, out result); return success ? result : null; @@ -113,7 +187,7 @@ public static EnumMapping Create(Type enumType) result._enumToLiteral.Add(value, literal); result._literalToEnum.Add(literal, value); - result._lowercaseLiteralToEnum.Add(literal.ToLowerInvariant(), value); + result._caseInsensitiveLiteralToEnum.Add(literal, value); } return result; diff --git a/src/Hl7.Fhir.Support.Tests/Utility/EnumMappingTest.cs b/src/Hl7.Fhir.Support.Tests/Utility/EnumMappingTest.cs index 41db9fee0c..94f0f89ab5 100644 --- a/src/Hl7.Fhir.Support.Tests/Utility/EnumMappingTest.cs +++ b/src/Hl7.Fhir.Support.Tests/Utility/EnumMappingTest.cs @@ -9,6 +9,7 @@ using FluentAssertions; using Hl7.Fhir.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Diagnostics; namespace Hl7.Fhir.Support.Tests.Utils @@ -35,6 +36,25 @@ public void TestCreation() Assert.IsNull(TestEnum.Item3.GetSystem()); } + [TestMethod] + public void TestCreationNonGeneric() + { + Assert.AreEqual("Testee", EnumUtility.GetName(typeof(TestEnum))); + Assert.AreEqual(TestEnum.Item1, EnumUtility.ParseLiteral("Item1", typeof(TestEnum))); + Assert.IsNull(EnumUtility.ParseLiteral("Item2", typeof(TestEnum))); + Assert.AreEqual(TestEnum.Item2, EnumUtility.ParseLiteral("ItemTwo", typeof(TestEnum))); + Assert.IsNull(EnumUtility.ParseLiteral("iTeM1", typeof(TestEnum))); + Assert.AreEqual(TestEnum.Item1, EnumUtility.ParseLiteral("iTeM1", typeof(TestEnum), ignoreCase: true)); + + Assert.AreEqual("Item1", ((Enum)TestEnum.Item1).GetLiteral()); + Assert.AreEqual("ItemTwo", ((Enum)TestEnum.Item2).GetLiteral()); + Assert.AreEqual("This is item two", TestEnum.Item2.GetDocumentation()); + Assert.AreEqual("http://example.org/test-system", ((Enum)TestEnum.Item2).GetSystem()); + Assert.AreEqual("ItemThree", ((Enum)TestEnum.Item3).GetLiteral()); + Assert.AreEqual("Item3", ((Enum)TestEnum.Item3).GetDocumentation()); + Assert.IsNull(((Enum)TestEnum.Item3).GetSystem()); + } + [FhirEnumeration("Testee")] enum TestEnum { @@ -107,6 +127,21 @@ public void EnumParsingPerformance() Assert.IsTrue(sw.ElapsedMilliseconds < 100); } + [TestMethod] + public void EnumParsingPerformanceNonGeneric() + { + var sw = new Stopwatch(); + sw.Start(); + + for (var i = 0; i < 10000; i++) + EnumUtility.ParseLiteral("male", typeof(TestAdministrativeGender)); + + sw.Stop(); + + Debug.WriteLine(sw.ElapsedMilliseconds); + Assert.IsTrue(sw.ElapsedMilliseconds < 100); + } + [TestMethod] public void TestEnumMapping() { @@ -118,14 +153,42 @@ public void TestEnumMapping() Assert.AreEqual("a", X.a.GetDocumentation()); // default documentation = name of item } + [TestMethod] + public void TestEnumMappingNonGeneric() + { + Assert.AreEqual(TestAdministrativeGender.Male, EnumUtility.ParseLiteral("male", typeof(TestAdministrativeGender))); + Assert.IsNull(EnumUtility.ParseLiteral("maleX", typeof(TestAdministrativeGender))); + Assert.AreEqual(X.a, EnumUtility.ParseLiteral("a", typeof(X))); + + Assert.AreEqual("Male", TestAdministrativeGender.Male.GetDocumentation()); + Assert.AreEqual("a", X.a.GetDocumentation()); // default documentation = name of item + } + [TestMethod] public void EnumLiteralPerformance() { var result = string.Empty; + var value = TestAdministrativeGender.Male; + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < 50_000; i++) + result = value.GetLiteral(); + sw.Stop(); + + Assert.AreEqual("male", result); + Debug.WriteLine(sw.ElapsedMilliseconds); + Assert.IsTrue(sw.ElapsedMilliseconds < 500); + } + + [TestMethod] + public void EnumLiteralPerformanceNonGeneric() + { + var result = string.Empty; + Enum value = TestAdministrativeGender.Male; var sw = Stopwatch.StartNew(); for (var i = 0; i < 50_000; i++) - result = TestAdministrativeGender.Male.GetLiteral(); + result = value.GetLiteral(); sw.Stop(); Assert.AreEqual("male", result);