From 6f8822252833b20c112019b294c7f8b3e3cd4b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Fri, 31 Dec 2021 21:58:17 +0100 Subject: [PATCH] feat: add support for conditional packet attribute --- ...acketConditionalIndexAttributeGenerator.cs | 299 ++++++++++++++++++ .../Data/AttributeInfo.cs | 15 +- .../Data/Parameters.cs | 3 +- .../Extensions/AttributeInfoExtensions.cs | 56 +++- .../PacketConverterGenerator.cs | 6 + .../SourceGenerator.cs | 12 +- .../PacketConditionalIndexAttribute.cs | 26 ++ Core/NosSmooth.Packets/Common/NameString.cs | 103 ++++++ .../Converters/Common/NameStringConverter.cs | 42 +++ .../Extensions/ServiceCollectionExtensions.cs | 2 + 10 files changed, 552 insertions(+), 12 deletions(-) create mode 100644 Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs create mode 100644 Core/NosSmooth.Packets/Common/NameString.cs create mode 100644 Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs diff --git a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..b334856ea1e14b724ae0cbfe4ff851d8e6cd574d --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs @@ -0,0 +1,299 @@ +// +// PacketConditionalIndexAttributeGenerator.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.CodeDom.Compiler; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; +using NosSmooth.PacketSerializersGenerator.Errors; +using NosSmooth.PacketSerializersGenerator.Extensions; + +namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators; + +/// +public class PacketConditionalIndexAttributeGenerator : IParameterGenerator +{ + /// + /// Gets the full name of the packet index attribute. + /// + public static string PacketConditionalIndexAttributeFullName + => "NosSmooth.Packets.Attributes.PacketConditionalIndexAttribute"; + + /// + public bool ShouldHandle(ParameterInfo parameter) + => parameter.Attributes.Any(x => x.FullName == PacketConditionalIndexAttributeFullName); + + /// + public IError? CheckParameter(PacketInfo packet, ParameterInfo parameter) + { + if (!parameter.Nullable) + { + return new DiagnosticError + ( + "SGNull", + "Conditional parameters must be nullable", + "The parameter {0} in {1} has to be nullable, because it is conditional.", + parameter.Parameter.SyntaxTree, + parameter.Parameter.FullSpan, + new List(new[] { parameter.Name, packet.Name }) + ); + } + + if (parameter.Attributes.Any(x => x.FullName != PacketConditionalIndexAttributeFullName)) + { + return new DiagnosticError + ( + "SGAttr", + "Packet constructor parameter with multiple packet attributes", + "Found multiple packet attributes of multiple types on parameter {0} in {1}. PacketConditionalIndexAttribute supports multiple attributes of the same type only.", + parameter.Parameter.SyntaxTree, + parameter.Parameter.FullSpan, + new List + ( + new[] + { + parameter.Name, + packet.Name + } + ) + ); + } + + // Check that all attributes have the same data. (where the same data need to be) + var firstAttribute = parameter.Attributes.First(); + if (parameter.Attributes.Any + ( + x => + { + var index = x.GetIndexedValue(0); + if (index != parameter.PacketIndex) + { + return true; + } + + foreach (var keyValue in x.NamedAttributeArguments) + { + if (!firstAttribute.NamedAttributeArguments.ContainsKey(keyValue.Key)) + { + return true; + } + + if (firstAttribute.NamedAttributeArguments[keyValue.Key] != keyValue.Value) + { + return true; + } + } + + return false; + } + )) + { + return new DiagnosticError + ( + "SGAttr", + "Packet constructor parameter with multiple conflicting attribute data.", + "Found multiple packet attributes of multiple types on parameter {0} in {1} with conflicting data. Index, IsOptional, InnerSeparator, AfterSeparator all have to be the same for each attribute.", + parameter.Parameter.SyntaxTree, + parameter.Parameter.FullSpan, + new List + ( + new[] + { + parameter.Name, + packet.Name + } + ) + ); + } + + var mismatchedAttribute = parameter.Attributes.FirstOrDefault + ( + x => x.IndexedAttributeArguments.Count < 4 || + (x.IndexedAttributeArguments[3].IsArray && + ( + (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax) + ?.Initializer is null || + (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax) + ?.Initializer?.Expressions.Count == 0 + ) + ) + ); + if (mismatchedAttribute is not null) + { + return new DiagnosticError + ( + "SGAttr", + "Packet conditional attribute without matching values", + "Found PacketConditionalIndexAttribute without matchValues parameters set on {0} in {1}. At least one parameter has to be specified.", + mismatchedAttribute.Attribute.SyntaxTree, + mismatchedAttribute.Attribute.FullSpan, + new List + ( + new[] + { + parameter.Name, + packet.Name + } + ) + ); + } + + return ParameterChecker.CheckOptionalIsNullable(packet, parameter); + } + + private string BuildAttributeIfPart(AttributeInfo attribute, string prefix) + { + var conditionParameterName = attribute.GetIndexedValue(1); + var negate = attribute.GetIndexedValue(2); + var values = attribute.GetParamsVisualValues(3); + if (conditionParameterName is null || values is null) + { + throw new ArgumentException(); + } + + var inner = string.Join + (" || ", values.Select(x => $"{prefix}{conditionParameterName.Trim('"')} == {x?.ToString() ?? "null"}")); + return (negate ? "!(" : string.Empty) + inner + (negate ? ")" : string.Empty); + } + + private string BuildParameterIfStatement(ParameterInfo parameter, string prefix) + { + var ifInside = string.Empty; + foreach (var attribute in parameter.Attributes) + { + ifInside += BuildAttributeIfPart(attribute, prefix); + } + + return $"if ({ifInside})"; + } + + /// + public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) + { + bool pushedLevel = false; + var generator = new ConverterSerializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attributes = parameter.Attributes; + var attribute = attributes.First(); + + // begin conditional if + textWriter.WriteLine(BuildParameterIfStatement(parameter, "obj.")); + textWriter.WriteLine("{"); + textWriter.Indent++; + + if (parameter.IsOptional()) + { + textWriter.WriteLine($"if (obj.{parameter.GetVariableName()} is not null)"); + textWriter.WriteLine("{"); + textWriter.Indent++; + } + + // register after separator + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) + { + generator.SetAfterSeparatorOnce((char)afterSeparator); + } + + // push inner separator level + var innerSeparator = attribute.GetNamedValue("InnerSeparator", null); + if (innerSeparator is not null) + { + generator.PushLevel((char)innerSeparator); + pushedLevel = true; + } + + // serialize, check the error. + generator.SerializeAndCheck(parameter); + + // pop inner separator level + if (pushedLevel) + { + generator.PopLevel(); + } + + // end optional if + if (parameter.IsOptional()) + { + textWriter.Indent--; + textWriter.WriteLine("}"); + } + + // end conditional if + textWriter.Indent--; + textWriter.WriteLine("}"); + + return null; + } + + /// + public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) + { + bool pushedLevel = false; + var generator = new ConverterDeserializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(); + + generator.DeclareLocalVariable(parameter); + + // begin conditional if + textWriter.WriteLine(BuildParameterIfStatement(parameter, string.Empty)); + textWriter.WriteLine("{"); + textWriter.Indent++; + + // add optional if + if (parameter.IsOptional()) + { + generator.StartOptionalCheck(parameter, packetInfo.Name); + } + + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) + { + generator.SetAfterSeparatorOnce((char)afterSeparator); + } + + var innerSeparator = attribute.GetNamedValue("InnerSeparator", null); + if (innerSeparator is not null) + { + generator.PushLevel((char)innerSeparator); + pushedLevel = true; + } + + generator.DeserializeAndCheck + ($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast); + + if (!parameter.Nullable) + { + generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name); + } + + generator.AssignLocalVariable(parameter, false); + if (pushedLevel) + { + generator.ReadToLastToken(); + generator.PopLevel(); + } + + // end is last token if body + if (parameter.IsOptional()) + { + generator.EndOptionalCheck(parameter); + } + + // end conditional if + textWriter.Indent--; + textWriter.WriteLine("}"); + textWriter.WriteLine("else"); + textWriter.WriteLine("{"); + textWriter.Indent++; + textWriter.WriteLine($"{parameter.GetVariableName()} = null;"); + textWriter.Indent--; + textWriter.WriteLine("}"); + + return null; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs index edba10e19acb294b120353c229485be9050fa4b2..d102c289bc26ca6ad943c7cc3d235c9867ec7183 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs @@ -19,6 +19,15 @@ public record AttributeInfo ( AttributeSyntax Attribute, string FullName, - IReadOnlyList IndexedAttributeArguments, - IReadOnlyDictionary NamedAttributeArguments -); \ No newline at end of file + IReadOnlyList IndexedAttributeArguments, + IReadOnlyDictionary NamedAttributeArguments +); + +/// +/// The attribute argument information. +/// +/// The argument syntax. +/// Whether the attribute argument is an array. +/// The real parsed value of the argument. +/// The visual value of the argument. +public record AttributeArgumentInfo(AttributeArgumentSyntax Argument, bool IsArray, object? RealValue, string VisualValue); \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs index bf3b49e3cccb617e5859783c396a7e961d60dea0..5efbb5e2da84f4b875119024f9704cf1d36a2d4c 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs @@ -4,6 +4,7 @@ // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using NosSmooth.PacketSerializersGenerator.AttributeGenerators; using NosSmooth.PacketSerializersGenerator.Extensions; namespace NosSmooth.PacketSerializersGenerator.Data; @@ -61,7 +62,7 @@ public class Parameters { for (int i = CurrentIndex + 1; i < List.Count; i++) { - if (!List[i].IsOptional()) + if (!List[i].IsOptional() && List[i].Attributes.All(x => x.FullName != PacketConditionalIndexAttributeGenerator.PacketConditionalIndexAttributeFullName)) { return false; } diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs index d7b9386e40126b4d37664330c264468fda73df96..000362d64bb7305f728e5714e9e71b250c24f7dc 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs @@ -5,6 +5,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Syntax; using NosSmooth.PacketSerializersGenerator.Data; namespace NosSmooth.PacketSerializersGenerator.Extensions; @@ -28,10 +29,10 @@ public static class AttributeInfoExtensions { if (typeof(TValue) == typeof(string)) { - return (TValue?)(object?)value?.ToString(); + return (TValue?)(object?)value.VisualValue; } - return (TValue?)value; + return (TValue?)value.RealValue; } return @default; @@ -50,9 +51,56 @@ public static class AttributeInfoExtensions if (typeof(TValue) == typeof(string)) { - return (TValue?)(object?)value?.ToString(); + return (TValue?)(object?)value?.VisualValue; } - return (TValue?)value; + return (TValue?)value.RealValue; + } + + /// + /// Gets visual values of params parameters in the constructor. + /// + /// The attribute info. + /// The index the values start at. + /// A list containing all the values. + public static IReadOnlyList GetParamsVisualValues(this AttributeInfo attributeInfo, int startingIndex) + { + if (attributeInfo.IndexedAttributeArguments.Count - 1 < startingIndex) + { + return Array.Empty(); + } + + if (attributeInfo.IndexedAttributeArguments[startingIndex].IsArray) + { + return attributeInfo.IndexedAttributeArguments[startingIndex].GetArrayVisualValues(); + } + + return attributeInfo.IndexedAttributeArguments + .Skip(startingIndex) + .Select(x => x.VisualValue) + .ToArray(); + } + + /// + /// Gets the visual values of the array. + /// + /// The attribute argument. + /// The list of the elements. + public static IReadOnlyList GetArrayVisualValues(this AttributeArgumentInfo attributeArgumentInfo) + { + var arrayCreation = attributeArgumentInfo.Argument.Expression as ArrayCreationExpressionSyntax; + if (arrayCreation is null) + { + throw new ArgumentException($"The given attribute argument is not an array creation.", nameof(attributeArgumentInfo)); + } + + if (arrayCreation.Initializer is null) + { + return Array.Empty(); + } + + return arrayCreation.Initializer.Expressions + .Select(x => x.ToString()) + .ToArray(); } } \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs index b3134ff0c7f7344f1242b4f12d9b571baa5f5939..c21d64e413d4ab26793f993fb981a5e2efb27c62 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs @@ -5,6 +5,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CodeDom.Compiler; +using Microsoft.CodeAnalysis.CSharp.Syntax; using NosSmooth.PacketSerializersGenerator.AttributeGenerators; using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; @@ -38,6 +39,10 @@ public class PacketConverterGenerator /// An error, if any. public IError? Generate(IndentedTextWriter textWriter) { + var usings = _packetInfo.PacketRecord.SyntaxTree.GetRoot() + .DescendantNodes() + .OfType(); + var usingsString = string.Join("\n", usings.Select(x => x.ToString())); textWriter.WriteLine ( @$"// @@ -49,6 +54,7 @@ using NosSmooth.Packets.Converters; using NosSmooth.Packets.Errors; using NosSmooth.Packets; using Remora.Results; +{usingsString} namespace {_packetInfo.Namespace}.Generated; diff --git a/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs index 4cece69a6bbcb442143ec905471146593d79b451..e4eeef5a2146cdb29aa8c7078581db79268d26c9 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs @@ -36,8 +36,10 @@ public class SourceGenerator : ISourceGenerator new IParameterGenerator[] { new PacketIndexAttributeGenerator(), + new PacketGreedyIndexAttributeGenerator(), new PacketListIndexAttributeGenerator(), new PacketContextListAttributeGenerator(), + new PacketConditionalIndexAttributeGenerator(), } ); } @@ -47,6 +49,7 @@ public class SourceGenerator : ISourceGenerator /// public void Initialize(GeneratorInitializationContext context) { + // SpinWait.SpinUntil(() => Debugger.IsAttached); } private IEnumerable GetPacketRecords(Compilation compilation, SyntaxTree tree) @@ -257,21 +260,22 @@ public class SourceGenerator : ISourceGenerator private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel) { - var namedArguments = new Dictionary(); - var arguments = new List(); + var namedArguments = new Dictionary(); + var arguments = new List(); foreach (var argument in attribute.ArgumentList!.Arguments) { var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString(); var value = argument.GetValue(semanticModel); + bool isArray = argument.Expression is ArrayCreationExpressionSyntax; if (argumentName is not null) { - namedArguments.Add(argumentName, value); + namedArguments.Add(argumentName, new AttributeArgumentInfo(argument, isArray, value, argument.ToString())); } else { - arguments.Add(value); + arguments.Add(new AttributeArgumentInfo(argument, isArray, value, argument.ToString())); } } diff --git a/Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs b/Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..1bb41d7a1cad56f2b6e8400f729401b5af0db2ef --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs @@ -0,0 +1,26 @@ +// +// PacketConditionalIndexAttribute.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Packets.Attributes; + +/// +/// Attribute for marking packet properties that may appear only in specific conditions. +/// +public class PacketConditionalIndexAttribute : PacketIndexAttribute +{ + /// + /// Initializes a new instance of the class. + /// You can use this attribute multiple times on one parameter. + /// + /// The position in the packet. + /// What parameter to check. (it has to precede this one). + /// Whether to negate the match values (not equals). + /// The values that mean this parameter is present. + public PacketConditionalIndexAttribute(ushort index, string conditionParameter, bool negate = false, params object?[] matchValues) + : base(index) + { + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Common/NameString.cs b/Core/NosSmooth.Packets/Common/NameString.cs new file mode 100644 index 0000000000000000000000000000000000000000..2abb16ec0a563e5686fe001fe131c38504963fd7 --- /dev/null +++ b/Core/NosSmooth.Packets/Common/NameString.cs @@ -0,0 +1,103 @@ +// +// NameString.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net.NetworkInformation; +using System.Reflection; + +namespace NosSmooth.Packets.Common; + +/// +/// Represents name in the game replacing "^" for " ". +/// +public class NameString +{ + /// + /// Gets the character used to separate words. + /// + public static char WordSeparator => '^'; + + /// + /// Creates instance from the given name retrieved from a packet. + /// + /// The name from the packet. + /// A name string instance. + public static NameString FromPacket(string packetName) + { + return new NameString(packetName, true); + } + + /// + /// Creates instance from the given name retrieved from a packet. + /// + /// The name from the packet. + /// A name string instance. + public static NameString FromString(string packetName) + { + return new NameString(packetName, true); + } + + private NameString(string name, bool packet) + { + if (packet) + { + PacketName = name; + Name = name.Replace(WordSeparator, ' '); + } + else + { + Name = name; + PacketName = name.Replace(' ', WordSeparator); + } + } + + /// + /// The real name. + /// + public string Name { get; } + + /// + /// The original name in the packet. + /// + public string PacketName { get; } + + /// + public override string ToString() + { + return Name; + } + + /// + /// Converts name string to regular string. + /// Returns the real name. + /// + /// The name string to convert. + /// The real name. + public static implicit operator string(NameString nameString) + { + return nameString.Name; + } + + /// + /// Converts regular string to name string. + /// + /// The string to convert. + /// The name string. + public static implicit operator NameString(string name) + { + return FromString(name); + } + + /// + public override bool Equals(object? obj) + { + if (!(obj is NameString nameString)) + { + return false; + } + + return Name.Equals(nameString.Name); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs b/Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs new file mode 100644 index 0000000000000000000000000000000000000000..00e6bb8d4a361684e99b4bec0162d644be28b71a --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs @@ -0,0 +1,42 @@ +// +// NameStringConverter.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Packets.Common; +using NosSmooth.Packets.Converters.Basic; +using Remora.Results; + +namespace NosSmooth.Packets.Converters.Common; + +/// +/// Converter of . +/// +public class NameStringConverter : BaseTypeConverter +{ + /// + public override Result Serialize(NameString? obj, PacketStringBuilder builder) + { + if (obj is null) + { + builder.Append("-"); + return Result.FromSuccess(); + } + + builder.Append(obj.PacketName); + return Result.FromSuccess(); + } + + /// + public override Result Deserialize(PacketStringEnumerator stringEnumerator) + { + var tokenResult = stringEnumerator.GetNextToken(); + if (!tokenResult.IsSuccess) + { + return Result.FromError(tokenResult); + } + + return NameString.FromPacket(tokenResult.Entity.Token); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs index 4eb6240f7b6aceeaf5ba1380b941f394b6eb6c39..27181c019eca2d31c7532e518087286ce6dfac25 100644 --- a/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using NosSmooth.Packets.Converters; using NosSmooth.Packets.Converters.Basic; +using NosSmooth.Packets.Converters.Common; using NosSmooth.Packets.Converters.Special; using NosSmooth.Packets.Packets; @@ -98,6 +99,7 @@ public static class ServiceCollectionExtensions .AddTypeConverter() .AddTypeConverter() .AddTypeConverter() + .AddTypeConverter() .AddTypeConverter(); }