From d4e6f39593d4048a378ea33b838622d857f42c96 Mon Sep 17 00:00:00 2001 From: Michael Einsiedler Date: Wed, 18 Nov 2020 14:52:28 +0100 Subject: [PATCH] Primitive Records (#15) * convert Audis.Primitives to records * make typeconverter only available for strings, add unit tests * Update azure-pipelines.yml for Azure Pipelines Use .NET 5 * Update azure-pipelines.yml for Azure Pipelines Use .NET 5 * add test-task for Audis.Primitives to pipelines * publish test results as TRX * use TRX logger for dotnet test --- azure-pipelines.yml | 15 ++ .../Audis.Primitives.Tests.csproj | 9 +- .../PrimitiveTypeConverterTests.cs | 43 ++++++ .../Audis.Primitives.Tests/PrimitivesTests.cs | 81 ++++++---- .../Audis.Primitives/Audis.Primitives.csproj | 15 +- .../Audis.Primitives/Audis.Primitives.xml | 50 ++++-- .../CaseInsensitiveStringPrimitive.cs | 32 ++++ .../CaseInsensitiveValueOfString.cs | 33 ---- .../Audis.Primitives/Primitive.cs | 15 ++ .../PrimitiveStringTypeConverter.cs | 84 +++++++++++ .../Audis.Primitives/Primitives.cs | 142 +++++++++--------- .../Audis.Primitives/ValueOf.cs | 104 ------------- .../Audis.Primitives/ValueOfTypeConverter.cs | 54 ------- 13 files changed, 361 insertions(+), 316 deletions(-) create mode 100644 src/Audis.Primitives/Audis.Primitives.Tests/PrimitiveTypeConverterTests.cs create mode 100644 src/Audis.Primitives/Audis.Primitives/CaseInsensitiveStringPrimitive.cs delete mode 100644 src/Audis.Primitives/Audis.Primitives/CaseInsensitiveValueOfString.cs create mode 100644 src/Audis.Primitives/Audis.Primitives/Primitive.cs create mode 100644 src/Audis.Primitives/Audis.Primitives/PrimitiveStringTypeConverter.cs delete mode 100644 src/Audis.Primitives/Audis.Primitives/ValueOf.cs delete mode 100644 src/Audis.Primitives/Audis.Primitives/ValueOfTypeConverter.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92cf13c..199e79a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,6 +15,12 @@ variables: buildConfiguration: 'Release' steps: +- task: UseDotNet@2 + displayName: 'Use .NET 5' + inputs: + packageType: sdk + version: '5.0.x' + - script: dotnet build --configuration $(buildConfiguration) src/Audis.Analyzer.Common/Audis.Analyzer.Common.sln displayName: Build Audis.Analyzer.Common @@ -33,6 +39,15 @@ steps: - script: dotnet build --configuration $(buildConfiguration) src/Audis.Primitives/Audis.Primitives.sln displayName: Build Audis.Primitives +- script: dotnet test --no-build --configuration $(buildConfiguration) --logger trx src/Audis.Primitives/Audis.Primitives.sln + displayName: Test Audis.Primitives + +- task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFormat: VSTest + testResultsFiles: '**/*.trx' + - task: CopyFiles@2 displayName: Publish NuGet Packages to Artifacts inputs: diff --git a/src/Audis.Primitives/Audis.Primitives.Tests/Audis.Primitives.Tests.csproj b/src/Audis.Primitives/Audis.Primitives.Tests/Audis.Primitives.Tests.csproj index ac84e63..9c62a07 100644 --- a/src/Audis.Primitives/Audis.Primitives.Tests/Audis.Primitives.Tests.csproj +++ b/src/Audis.Primitives/Audis.Primitives.Tests/Audis.Primitives.Tests.csproj @@ -2,7 +2,8 @@ Exe - netcoreapp3.0 + net5.0 + enable @@ -15,11 +16,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Audis.Primitives/Audis.Primitives.Tests/PrimitiveTypeConverterTests.cs b/src/Audis.Primitives/Audis.Primitives.Tests/PrimitiveTypeConverterTests.cs new file mode 100644 index 0000000..75dd56a --- /dev/null +++ b/src/Audis.Primitives/Audis.Primitives.Tests/PrimitiveTypeConverterTests.cs @@ -0,0 +1,43 @@ +using System; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Audis.Primitives.Tests +{ + [TestFixture] + public class PrimitiveTypeConverterTests + { + [Test] + public void StringSerialization() + { + var knowledgeValue = new KnowledgeValue("asdf"); + var dto = new StringDto { Primitive = knowledgeValue }; + + var json = JsonConvert.SerializeObject(dto); + var newDto = JsonConvert.DeserializeObject(json); + + Assert.That(json, Is.EqualTo("{\"Primitive\":\"asdf\"}")); + Assert.That(newDto.Primitive, Is.EqualTo(dto.Primitive)); + Assert.That(newDto.Primitive!.Value, Is.EqualTo(knowledgeValue.Value)); + } + + [Test] + public void KnowledgeValueWithDateTimeCanBeSerializedAndDeserialized() + { + var date = new DateTime(2020, 11, 11, 11, 11, 11); + var knowledgeValue = new KnowledgeValue(date.ToString("o")); + var json = JsonConvert.SerializeObject(knowledgeValue); + + var deserializedKnowledegeValue = JsonConvert.DeserializeObject(json); + + Assert.That(json, Is.EqualTo("\"2020-11-11T11:11:11.0000000\"")); + Assert.That(deserializedKnowledegeValue, Is.EqualTo(knowledgeValue)); + Assert.That(deserializedKnowledegeValue.Value, Is.EqualTo(date.ToString("o"))); + } + + public class StringDto + { + public KnowledgeValue? Primitive { get; set; } + } + } +} diff --git a/src/Audis.Primitives/Audis.Primitives.Tests/PrimitivesTests.cs b/src/Audis.Primitives/Audis.Primitives.Tests/PrimitivesTests.cs index aa9fc75..cd405ef 100644 --- a/src/Audis.Primitives/Audis.Primitives.Tests/PrimitivesTests.cs +++ b/src/Audis.Primitives/Audis.Primitives.Tests/PrimitivesTests.cs @@ -1,5 +1,4 @@ using System; -using Newtonsoft.Json; using NUnit.Framework; namespace Audis.Primitives.Tests @@ -12,7 +11,7 @@ public class PrimitivesTests [TestCase("@rd4", "rd4")] public void TestDispositionCode(string input, string expected) { - Assert.AreEqual(expected, DispositionCode.From(input).Value); + Assert.AreEqual(expected, new DispositionCode(input).Value); } [TestCase(null)] @@ -21,7 +20,7 @@ public void TestDispositionCode(string input, string expected) [TestCase("@")] public void TestEmptyDispositionCodeThrows(string input) { - Assert.Throws(() => DispositionCode.From(input)); + Assert.Throws(() => new DispositionCode(input)); } [TestCase(null)] @@ -29,13 +28,13 @@ public void TestEmptyDispositionCodeThrows(string input) [TestCase(" ")] public void TestEmptyKnowledgeIdentifierThrows(string input) { - Assert.Throws(() => KnowledgeIdentifier.From(input)); + Assert.Throws(() => new KnowledgeIdentifier(input)); } [Test] public void TestKnowledgeIdentifierInvalidFormat() { - var ex = Assert.Throws(() => KnowledgeIdentifier.From("no-leading-hashtag")); + var ex = Assert.Throws(() => new KnowledgeIdentifier("no-leading-hashtag")); Assert.AreEqual("The KnowledgeIdentifier has an invalid format: \"no-leading-hashtag\", Expected starting with #.", ex.Message); } @@ -44,7 +43,7 @@ public void TestKnowledgeIdentifierInvalidFormat() [TestCase(" ")] public void TestEmptyKnowledgeValueThrows(string input) { - Assert.Throws(() => KnowledgeValue.From(input)); + Assert.Throws(() => new KnowledgeValue(input)); } [TestCase(null)] @@ -52,7 +51,7 @@ public void TestEmptyKnowledgeValueThrows(string input) [TestCase(" ")] public void TestEmptyQuestionIdThrows(string input) { - Assert.Throws(() => QuestionId.From(input)); + Assert.Throws(() => new QuestionId(input)); } [TestCase("invalid-format", true)] @@ -65,23 +64,32 @@ public void TestQuestionIdInvalidFormat(string input, bool throws) { if (throws) { - var ex = Assert.Throws(() => QuestionId.From(input)); - Assert.AreEqual($"The QuestionId has an invalid format: \"{input}\", Expected \":\".", ex.Message); + var ex = Assert.Throws(() => new QuestionId(input)); + Assert.AreEqual($"The QuestionId has an invalid format: \"{input}\", Expected \":\".", ex.Message); } else { - var questionId = QuestionId.From(input); + var questionId = new QuestionId(input); Assert.AreEqual(input, questionId.Value); - Assert.AreEqual(QuestionCatalogName.From(input.Split(':')[0]), questionId.ConfigurationName); + Assert.AreEqual(new QuestionCatalogName(input.Split(':')[0]), questionId.QuestionCatalogName); } } + [Test] + public void TestQuestionIdQuestionCatalogAndLineNumberConstructor() + { + var questionId = new QuestionId(new QuestionCatalogName("question-catalog"), 10); + Assert.That(questionId.Value, Is.EqualTo("question-catalog:10")); + Assert.AreEqual(new QuestionCatalogName(questionId.Value.Split(':')[0]), questionId.QuestionCatalogName); + Assert.AreEqual(int.Parse(questionId.Value.Split(':')[1]), questionId.LineNumber); + } + [TestCase(null)] [TestCase("")] [TestCase(" ")] public void TestEmptyAnswerIdThrows(string input) { - Assert.Throws(() => AnswerId.From(input)); + Assert.Throws(() => new AnswerId(input)); } [TestCase("invalid-format", true)] @@ -97,26 +105,35 @@ public void TestAnswerIdInvalidFormat(string input, bool throws) { if (throws) { - var ex = Assert.Throws(() => AnswerId.From(input)); - Assert.AreEqual($"The AnswerId has an invalid format: \"{input}\", Expected \":/\".", ex.Message); + var ex = Assert.Throws(() => new AnswerId(input)); + Assert.AreEqual($"The AnswerId has an invalid format: \"{input}\", Expected \":/\".", ex.Message); } else { - var answerId = AnswerId.From(input); + var answerId = new AnswerId(input); Assert.AreEqual(input, answerId.Value); var split = input.Split('/'); Assert.AreEqual(split[0], answerId.QuestionId.Value); } } + [Test] + public void TestAnswerIdQuestionIdAndLineNumberConstructor() + { + var answerId = new AnswerId(new QuestionId(new QuestionCatalogName("question-catalog"), 10), 12); + Assert.That(answerId.Value, Is.EqualTo("question-catalog:10/12")); + Assert.AreEqual(new QuestionId(answerId.Value.Split('/')[0]), answerId.QuestionId); + Assert.AreEqual(int.Parse(answerId.Value.Split('/')[1]), answerId.LineNumber); + } + [TestCase("#audis.schmerzen", "#audis.schmerzen", true)] [TestCase("#audis.SCHMERZEN", "#audis.schmerzen", true)] [TestCase("#audis.schmerzen", "#audis.SCHMERZEN", true)] [TestCase("#audis.value1", "#audis.value2", false)] public void AssertKnowledgeIdentifierCaseInsensitiveEqualsAndGetHashCode(string input1, string input2, bool equals) { - var value1 = KnowledgeIdentifier.From(input1); - var value2 = KnowledgeIdentifier.From(input2); + var value1 = new KnowledgeIdentifier(input1); + var value2 = new KnowledgeIdentifier(input2); Assert.AreEqual(equals, value1.Equals(value2)); Assert.AreEqual(equals, value1.GetHashCode() == value2.GetHashCode()); @@ -129,8 +146,9 @@ public void AssertKnowledgeIdentifierCaseInsensitiveEqualsAndGetHashCode(string [TestCase("ja", "nein", false)] public void AssertKnowledgeValueCaseInsensitiveEqualsAndGetHashCode(string input1, string input2, bool equals) { - var value1 = KnowledgeValue.From(input1); - var value2 = KnowledgeValue.From(input2); + var v = new KnowledgeValue("asdf"); + var value1 = new KnowledgeValue(input1); + var value2 = new KnowledgeValue(input2); Assert.AreEqual(equals, value1.Equals(value2)); Assert.AreEqual(equals, value1.GetHashCode() == value2.GetHashCode()); @@ -145,20 +163,27 @@ public void AssertKnowledgeValueCaseInsensitiveEqualsAndGetHashCode(string input [TestCase("@RD1", "@RD2", false)] public void AssertDispositionCodeCaseInsensitiveEqualsAndGetHashCode(string input1, string input2, bool equals) { - var value1 = DispositionCode.From(input1); - var value2 = DispositionCode.From(input2); + var value1 = new DispositionCode(input1); + var value2 = new DispositionCode(input2); Assert.AreEqual(equals, value1.Equals(value2)); Assert.AreEqual(equals, value1.GetHashCode() == value2.GetHashCode()); } - [Test] - public void KnowledgeValueWithDateTimeCanBeSerializedAndDeserialized() + [TestCase("@Scenario", "@Scenario", true)] + [TestCase("@Scenario", "Scenario", true)] + [TestCase("Scenario", "@Scenario", true)] + [TestCase("Scenario", "Scenario", true)] + [TestCase("scenario", "SCENARIO", true)] + [TestCase("SCENARIO", "scenario", true)] + [TestCase("@Scenario1", "@Scenario2", false)] + public void AssertScenarioCaseInsensitiveEqualsAndGetHashCode(string input1, string input2, bool equals) { - var knowledgeValue = KnowledgeValue.From(DateTime.Now.ToString("o")); - var json = JsonConvert.SerializeObject(knowledgeValue); - var deserializedKnowledegeValue = JsonConvert.DeserializeObject(json); - Assert.AreEqual(knowledgeValue, deserializedKnowledegeValue); + var value1 = new ScenarioIdentifier(input1); + var value2 = new ScenarioIdentifier(input2); + + Assert.AreEqual(equals, value1.Equals(value2)); + Assert.AreEqual(equals, value1.GetHashCode() == value2.GetHashCode()); } } -} +} \ No newline at end of file diff --git a/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.csproj b/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.csproj index 80bc250..233a0e2 100644 --- a/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.csproj +++ b/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net5.0 true softaware gmbh softaware gmbh @@ -12,9 +12,10 @@ git https://github.com/softawaregmbh/audis-public Audis - 1.0.1.0 - 1.0.1.0 - 1.0.1 + 2.0.0.0 + 2.0.0.0 + 2.0.0 + enable @@ -23,12 +24,12 @@ - D:\_GIT\work\audis-public\src\Audis.Primitives\Audis.Primitives\Audis.Primitives.xml + Audis.Primitives.xml 1701;1702;1591 - C:\Development\audis-public\src\Audis.Primitives\Audis.Primitives.xml + Audis.Primitives.xml 1701;1702;1591 @@ -41,7 +42,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.xml b/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.xml index cfb7b79..f666329 100644 --- a/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.xml +++ b/src/Audis.Primitives/Audis.Primitives/Audis.Primitives.xml @@ -4,37 +4,55 @@ Audis.Primitives - + - A where the underlying type is a . + A where the underlying type is a . The comparision with other strings is case-insensitive. - Values must not be null. + Values must not be . - + - Identifies a question with and ("questionConfigurationName:LineNumber"). + Base record for wrapping primitive types. + Used to prevent primitive obsession, see https://refactoring.guru/smells/primitive-obsession. + The type of the wrapped value. - + - Identifies an answer with , - and ("questionConfigurationName:LineNumber/KnowledgeValue"). + Base record for wrapping primitive types. + Used to prevent primitive obsession, see https://refactoring.guru/smells/primitive-obsession. + The type of the wrapped value. - + - Base class for wrapping primitive types. - Used to prevent primitive obsession, see https://refactoring.guru/smells/primitive-obsession. + Identifies a question with and ("questionCatalogName:LineNumber"). + + + + + Identifies an answer with and ("questionCatalogName:questionLineNumber/answerLineNumber"). - The primitive type to wrap. - The wrapping type itself. - + - Type converter for types to allow seamingless serialization and deserialization. - Use this type as parameter for the for subclasses of . + Type converter for where TValue is to allow seamingless serialization and deserialization. + Use this type as parameter for the for subclasses of . + The primitive type inherited from . + + + + + + + + + + + + diff --git a/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveStringPrimitive.cs b/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveStringPrimitive.cs new file mode 100644 index 0000000..141c4f2 --- /dev/null +++ b/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveStringPrimitive.cs @@ -0,0 +1,32 @@ +using System; + +namespace Audis.Primitives +{ + /// + /// A where the underlying type is a . + /// The comparision with other strings is case-insensitive. + /// Values must not be . + /// + public record CaseInsensitiveStringPrimitive + : Primitive + { + public CaseInsensitiveStringPrimitive(string value) + : base(value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException("Value must not be null or whitespace."); + } + } + + public virtual bool Equals(CaseInsensitiveStringPrimitive? other) + { + return other is not null && this.Value.Equals(other.Value, StringComparison.InvariantCultureIgnoreCase); + } + + public override int GetHashCode() + { + return this.Value.ToLowerInvariant().GetHashCode(); + } + } +} diff --git a/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveValueOfString.cs b/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveValueOfString.cs deleted file mode 100644 index 2cf4d1d..0000000 --- a/src/Audis.Primitives/Audis.Primitives/CaseInsensitiveValueOfString.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Audis.Primitives -{ - /// - /// A where the underlying type is a . - /// The comparision with other strings is case-insensitive. - /// Values must not be null. - /// - public class CaseInsensitiveValueOfString : ValueOf - where TThis : ValueOf, new() - { - protected override void Validate() - { - if (string.IsNullOrWhiteSpace(this.Value)) - { - throw new ArgumentNullException(nameof(this.Value)); - } - } - - protected override bool Equals(ValueOf other) - { - return EqualityComparer.Default.Equals(this.Value.ToLowerInvariant(), other.Value.ToLowerInvariant()); - } - - public override int GetHashCode() - { - return EqualityComparer.Default.GetHashCode(this.Value.ToLowerInvariant()); - } - } -} diff --git a/src/Audis.Primitives/Audis.Primitives/Primitive.cs b/src/Audis.Primitives/Audis.Primitives/Primitive.cs new file mode 100644 index 0000000..d09fc0d --- /dev/null +++ b/src/Audis.Primitives/Audis.Primitives/Primitive.cs @@ -0,0 +1,15 @@ +namespace Audis.Primitives +{ + /// + /// Base record for wrapping primitive types. + /// Used to prevent primitive obsession, see https://refactoring.guru/smells/primitive-obsession. + /// + /// The type of the wrapped value. + public record Primitive(TValue Value) + { + public override string? ToString() + { + return this.Value?.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Audis.Primitives/Audis.Primitives/PrimitiveStringTypeConverter.cs b/src/Audis.Primitives/Audis.Primitives/PrimitiveStringTypeConverter.cs new file mode 100644 index 0000000..8433221 --- /dev/null +++ b/src/Audis.Primitives/Audis.Primitives/PrimitiveStringTypeConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Audis.Primitives +{ + /// + /// Type converter for where TValue is to allow seamingless serialization and deserialization. + /// Use this type as parameter for the for subclasses of . + /// + /// The primitive type inherited from . + public class PrimitiveStringTypeConverter : TypeConverter + where TPrimitive : Primitive + { + private static readonly Func PrimitiveTypeInstanceCreator; + + static PrimitiveStringTypeConverter() + { + var ctor = typeof(TPrimitive).GetTypeInfo().DeclaredConstructors.First(c => + { + // Each Primitive has at least one constructor with one single parameter of type TValue (string). + var parameters = c.GetParameters(); + return parameters.Length == 1 && parameters[0].ParameterType == typeof(string); + }); + + var tValueParameter = Expression.Parameter(typeof(string)); + NewExpression newExp = Expression.New(ctor, tValueParameter); + LambdaExpression lambda = Expression.Lambda(typeof(Func), newExp, tValueParameter); + PrimitiveTypeInstanceCreator = (Func)lambda.Compile(); + } + + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string) || sourceType == typeof(DateTime)) + { + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + /// + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string stringValue) + { + return PrimitiveTypeInstanceCreator(stringValue); + } + + if (value is DateTime date && date.ToString("o") is string dateStringValue) + { + return PrimitiveTypeInstanceCreator(dateStringValue); + } + + return base.ConvertFrom(context, culture, value); + } + + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + if (destinationType == typeof(string)) + { + return true; + } + + return base.CanConvertTo(context, destinationType); + } + + /// + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (value is Primitive t && (destinationType == typeof(string) || destinationType == typeof(DateTime))) + { + return t.Value; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/src/Audis.Primitives/Audis.Primitives/Primitives.cs b/src/Audis.Primitives/Audis.Primitives/Primitives.cs index 6ac508a..8e8348a 100644 --- a/src/Audis.Primitives/Audis.Primitives/Primitives.cs +++ b/src/Audis.Primitives/Audis.Primitives/Primitives.cs @@ -1,44 +1,46 @@ #pragma warning disable SA1649 // File name should match first type name -#pragma warning disable SA1402 // File may only contain a single type -#pragma warning disable SA1502 // Element should not be on a single line using System; using System.ComponentModel; using System.Text.RegularExpressions; namespace Audis.Primitives { + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record TenantId(string Value) + : CaseInsensitiveStringPrimitive(Value); + /// /// When you want to store a instance as , /// make sure it is serialized conforming to ISO_8601 (DateTime.ToString("o")). /// - [TypeConverter(typeof(ValueOfTypeConverter))] - public class KnowledgeValue : CaseInsensitiveValueOfString { } - - [TypeConverter(typeof(ValueOfTypeConverter))] - public class TenantId : CaseInsensitiveValueOfString { } + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record KnowledgeValue(string Value) + : CaseInsensitiveStringPrimitive(Value); - [TypeConverter(typeof(ValueOfTypeConverter))] - public class RevisionId : CaseInsensitiveValueOfString { } + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record RevisionId(string Value) + : CaseInsensitiveStringPrimitive(Value); - [TypeConverter(typeof(ValueOfTypeConverter))] - public class QuestionCatalogName : CaseInsensitiveValueOfString { } + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record QuestionCatalogName(string Value) + : CaseInsensitiveStringPrimitive(Value); - [TypeConverter(typeof(ValueOfTypeConverter))] - public class Operator : ValueOf + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record Operator(string Value) + : Primitive(Value) { - public static Operator AndOperator = Operator.From("&&"); - public static Operator OrOperator = Operator.From("||"); - public static Operator EqualsOperator = Operator.From("="); - public static Operator UnequalsOperator = Operator.From("!="); + public static readonly Operator AndOperator = new Operator("&&"); + public static readonly Operator OrOperator = new Operator("||"); + public static readonly Operator EqualsOperator = new Operator("="); + public static readonly Operator UnequalsOperator = new Operator("!="); } - [TypeConverter(typeof(ValueOfTypeConverter))] - public class KnowledgeIdentifier : CaseInsensitiveValueOfString + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record KnowledgeIdentifier : CaseInsensitiveStringPrimitive { - protected override void Validate() + public KnowledgeIdentifier(string value) + : base(value) { - base.Validate(); - if (!this.Value.StartsWith("#")) { throw new ArgumentException($"The {nameof(KnowledgeIdentifier)} has an invalid format: \"{this.Value}\", Expected starting with #."); @@ -47,98 +49,98 @@ protected override void Validate() } /// - /// Identifies a question with and ("questionConfigurationName:LineNumber"). + /// Identifies a question with and ("questionCatalogName:LineNumber"). /// - [TypeConverter(typeof(ValueOfTypeConverter))] - public class QuestionId : CaseInsensitiveValueOfString + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record QuestionId + : CaseInsensitiveStringPrimitive { - public QuestionCatalogName ConfigurationName { get; private set; } - public int LineNumber { get; private set; } - - protected override void Validate() + public QuestionId(string value) + : base(value) { - base.Validate(); - if (!Regex.IsMatch(this.Value, @"[\wäüöß-]+:[1-9]\d*")) // configurationName:anyNonZeroDigit { - throw new ArgumentException($"The {nameof(QuestionId)} has an invalid format: \"{this.Value}\", Expected \":\"."); + throw new ArgumentException($"The {nameof(QuestionId)} has an invalid format: \"{this.Value}\", Expected \":\"."); } var split = this.Value.Split(':'); - this.ConfigurationName = QuestionCatalogName.From(split[0]); + this.QuestionCatalogName = new QuestionCatalogName(split[0]); this.LineNumber = int.Parse(split[1]); } - public static QuestionId From(QuestionCatalogName configurationName, int lineNumber) + public QuestionId(QuestionCatalogName questionCatalogName, int lineNumber) + : this($"{questionCatalogName.Value}:{lineNumber}") { - return QuestionId.From($"{configurationName.Value}:{lineNumber}"); } + + public int LineNumber { get; } + public QuestionCatalogName QuestionCatalogName { get; } } /// - /// Identifies an answer with and ("questionConfigurationName:questionLineNumber/answerLineNumber"). + /// Identifies an answer with and ("questionCatalogName:questionLineNumber/answerLineNumber"). /// - [TypeConverter(typeof(ValueOfTypeConverter))] - public class AnswerId : CaseInsensitiveValueOfString + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record AnswerId + : CaseInsensitiveStringPrimitive { - public QuestionId QuestionId { get; private set; } - public int LineNumber { get; private set; } - - protected override void Validate() + public AnswerId(string value) + : base(value) { - base.Validate(); - if (!Regex.IsMatch(this.Value, @"[\wäüöß-]+:[1-9]\d*\/\d+")) // e.g. "abcde:10/3 { - throw new ArgumentException($"The {nameof(AnswerId)} has an invalid format: \"{this.Value}\", Expected \":/\"."); + throw new ArgumentException($"The {nameof(AnswerId)} has an invalid format: \"{this.Value}\", Expected \":/\"."); } var split = this.Value.Split('/'); - this.QuestionId = QuestionId.From(split[0]); + this.QuestionId = new QuestionId(split[0]); this.LineNumber = int.Parse(split[1]); } - public static AnswerId From(QuestionId questionId, int lineNumber) + public AnswerId(QuestionId questionId, int lineNumber) + : this($"{questionId.Value}/{lineNumber}") { - return AnswerId.From($"{questionId.Value}/{lineNumber}"); } + + public QuestionId QuestionId { get; } + public int LineNumber { get; } } - [TypeConverter(typeof(ValueOfTypeConverter))] - public class DispositionCode : CaseInsensitiveValueOfString + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record DispositionCode + : CaseInsensitiveStringPrimitive { - protected override string Create(string value) + public DispositionCode(string value) + : base(value) { - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentNullException(nameof(value)); - } - if (value[0] == '@') { - value = value.Substring(1); - } + if (value.Length == 1) + { + throw new ArgumentNullException("value must not be empty."); + } - return value; + this.Value = value.Substring(1); + } } } - [TypeConverter(typeof(ValueOfTypeConverter))] - public class ScenarioIdentifier : CaseInsensitiveValueOfString + [TypeConverter(typeof(PrimitiveStringTypeConverter))] + public record ScenarioIdentifier + : CaseInsensitiveStringPrimitive { - protected override string Create(string value) + public ScenarioIdentifier(string value) + : base(value) { - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentNullException(nameof(value)); - } - if (value[0] == '@') { - value = value.Substring(1); - } + if (value.Length == 1) + { + throw new ArgumentNullException("value must not be empty."); + } - return value; + this.Value = value.Substring(1); + } } } } diff --git a/src/Audis.Primitives/Audis.Primitives/ValueOf.cs b/src/Audis.Primitives/Audis.Primitives/ValueOf.cs deleted file mode 100644 index 7438cf9..0000000 --- a/src/Audis.Primitives/Audis.Primitives/ValueOf.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace Audis.Primitives -{ - /// - /// Base class for wrapping primitive types. - /// Used to prevent primitive obsession, see https://refactoring.guru/smells/primitive-obsession. - /// - /// The primitive type to wrap. - /// The wrapping type itself. - public class ValueOf - where TThis : ValueOf, new() - { - private static readonly Func Factory; - - protected virtual void Validate() - { - } - - protected virtual TValue Create(TValue value) - { - return value; - } - - static ValueOf() - { - var ctor = typeof(TThis).GetTypeInfo().DeclaredConstructors.First(); - Expression[] argsExp = new Expression[0]; - NewExpression newExp = Expression.New(ctor, argsExp); - LambdaExpression lambda = Expression.Lambda(typeof(Func), newExp); - Factory = (Func)lambda.Compile(); - } - - public TValue Value { get; protected set; } - - public static TThis From(TValue item) - { - var x = Factory(); - x.Value = x.Create(item); - x.Validate(); - return x; - } - - protected virtual bool Equals(ValueOf other) - { - return EqualityComparer.Default.Equals(this.Value, other.Value); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return this.Equals((ValueOf)obj); - } - - public override int GetHashCode() - { - return EqualityComparer.Default.GetHashCode(this.Value); - } - - public static bool operator ==(ValueOf a, ValueOf b) - { - if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) - { - return true; - } - - if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) - { - return false; - } - - return a.Equals(b); - } - - public static bool operator !=(ValueOf a, ValueOf b) - { - return !(a == b); - } - - public override string ToString() - { - return this.Value.ToString(); - } - } -} diff --git a/src/Audis.Primitives/Audis.Primitives/ValueOfTypeConverter.cs b/src/Audis.Primitives/Audis.Primitives/ValueOfTypeConverter.cs deleted file mode 100644 index 77c5e27..0000000 --- a/src/Audis.Primitives/Audis.Primitives/ValueOfTypeConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; - -namespace Audis.Primitives -{ - /// - /// Type converter for types to allow seamingless serialization and deserialization. - /// Use this type as parameter for the for subclasses of . - /// - public class ValueOfTypeConverter : TypeConverter - where TThis : ValueOf, new() - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(TValue)) - { - return true; - } - - if (sourceType == typeof(DateTime) && typeof(TValue) == typeof(string)) - { - return true; - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is TValue tValue) - { - return ValueOf.From(tValue); - } - - if (value is DateTime date && typeof(TValue) == typeof(string) && date.ToString("o") is TValue stringValue) - { - return ValueOf.From(stringValue); - } - - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(TValue)) - { - return ((TThis)value).Value; - } - - return base.ConvertTo(context, culture, value, destinationType); - } - } -}