From 43fb16ed35dc41f466a2fcdfc89d8bf7ee1d48ed Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Tue, 18 Jun 2024 17:39:05 +0400 Subject: [PATCH 1/2] Added support to Generate EntityFrameworkCore ValueConverters --- ...AltaSoft.DomainPrimitives.Generator.csproj | 1 + .../DomainPrimitiveGenerator.cs | 6 ++ .../Executor.cs | 11 +++ .../Helpers/MethodGeneratorHelper.cs | 86 +++++++++++++++++++ .../Models/DomainPrimitiveGlobalOptions.cs | 10 ++- .../TestHelpers.cs | 10 ++- 6 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.csproj b/src/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.csproj index 0cfbd55..f39f625 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.csproj +++ b/src/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.csproj @@ -26,6 +26,7 @@ + diff --git a/src/AltaSoft.DomainPrimitives.Generator/DomainPrimitiveGenerator.cs b/src/AltaSoft.DomainPrimitives.Generator/DomainPrimitiveGenerator.cs index e6af9cc..2426a89 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/DomainPrimitiveGenerator.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/DomainPrimitiveGenerator.cs @@ -119,6 +119,12 @@ private static DomainPrimitiveGlobalOptions GetGlobalOptions(AnalyzerConfigOptio result.GenerateXmlSerialization = generateXmlSerialization; } + if (analyzerOptions.GlobalOptions.TryGetValue("build_property.DomainPrimitiveGenerator_GenerateEntityFrameworkCoreValueConverters", out value) + && bool.TryParse(value, out var generateEntityFrameworkValueConverters)) + { + result.GenerateEntityFrameworkCoreValueConverters = generateEntityFrameworkValueConverters; + } + return result; } } diff --git a/src/AltaSoft.DomainPrimitives.Generator/Executor.cs b/src/AltaSoft.DomainPrimitives.Generator/Executor.cs index 8f38ec7..688090e 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Executor.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Executor.cs @@ -33,6 +33,7 @@ internal static void Execute( return; var swaggerTypes = new List(typesToGenerate.Length); + var efCoreValueConverterTypes = new List(typesToGenerate.Length); var cachedOperationsAttributes = new Dictionary(SymbolEqualityComparer.Default); try @@ -71,10 +72,20 @@ internal static void Execute( } if (globalOptions.GenerateSwaggerConverters) + { swaggerTypes.Add(generatorData); + } + + if (globalOptions.GenerateEntityFrameworkCoreValueConverters) + { + efCoreValueConverterTypes.Add(generatorData.TypeSymbol); + MethodGeneratorHelper.ProcessEntityFrameworkValueConverter(generatorData, context); + } } MethodGeneratorHelper.AddSwaggerOptions(assemblyName, swaggerTypes, context); + + MethodGeneratorHelper.GenerateValueConvertersExtension(swaggerTypes.Count == 0, assemblyName, efCoreValueConverterTypes, context); } catch (Exception ex) { diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index be35527..61103b7 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -186,6 +186,92 @@ internal static void ProcessTypeConverter(GeneratorData data, SourceProductionCo context.AddSource($"{data.ClassName}TypeConverter.g.cs", builder.ToString()); } + /// + /// Generates the value converters extension for the specified assembly name, types, and source production context. + /// + /// if assembly attribute should be added + /// The name of the assembly. + /// The list of named type symbols. + /// The source production context. + internal static void GenerateValueConvertersExtension(bool addAssemblyAttribute, string assemblyName, List types, SourceProductionContext context) + { + if (types.Count == 0) + return; + + var builder = new SourceCodeBuilder(); + builder.AppendSourceHeader("AltaSoft DomainPrimitives Generator"); + + var usings = types.ConvertAll(x => x.ContainingNamespace.ToDisplayString()); + usings.Add("Microsoft.EntityFrameworkCore"); + usings.AddRange(types.ConvertAll(x => x.ContainingNamespace.ToDisplayString() + ".EntityFrameworkCore.Converters")); + + builder.AppendUsings(usings); + + if (addAssemblyAttribute) + builder.AppendLine("[assembly: AltaSoft.DomainPrimitives.DomainPrimitiveAssemblyAttribute]"); + + var ns = string.Join(".", assemblyName.Split('.').Select(s => char.IsDigit(s[0]) ? '_' + s : s)); + builder.AppendNamespace(ns + ".Converters.Extensions"); + + builder.AppendSummary($"Helper class providing methods to configure EntityFrameworkCore ValueConverters for DomainPrimitive types of {assemblyName}"); + builder.AppendClass(false, "public static", "ModelConfigurationBuilderExt"); + + builder.AppendSummary("Adds EntityFrameworkCore ValueConverters for specific custom types to ensure proper mapping to EFCore ORM."); + builder.AppendParamDescription("configurationBuilder", "The ModelConfigurationBuilder instance to which converters are added."); + builder.AppendLine("public static ModelConfigurationBuilder AddDomainPrimitivePropertyConversions(this ModelConfigurationBuilder configurationBuilder)") + .OpenBracket(); + + foreach (var type in types) + { + builder.Append("configurationBuilder.Properties<").Append(type.Name).Append(">().HaveConversion<").Append(type.Name).AppendLine("ValueConverter>();"); + } + + builder.AppendLine("return configurationBuilder;"); + builder.CloseBracket(); + + builder.CloseBracket(); + context.AddSource("ModelConfigurationBuilderExt.g.cs", builder.ToString()); + } + + /// + /// Processes the Entity Framework value converter for the specified generator data and source production context. + /// + /// The generator data. + /// The source production context. + internal static void ProcessEntityFrameworkValueConverter(GeneratorData data, SourceProductionContext context) + { + var builder = new SourceCodeBuilder(); + + builder.AppendSourceHeader("AltaSoft DomainPrimitives Generator"); + + var usingStatements = + new List(8) + { + data.Namespace, + data.PrimitiveTypeSymbol.ContainingNamespace.ToDisplayString(), + "Microsoft.EntityFrameworkCore", + "Microsoft.EntityFrameworkCore.Storage.ValueConversion", + "AltaSoft.DomainPrimitives", + }; + + var converterName = data.ClassName + "ValueConverter"; + + builder.AppendUsings(usingStatements); + + builder.AppendNamespace(data.Namespace + ".EntityFrameworkCore.Converters"); + builder.AppendSummary($"ValueConverter for "); + builder.AppendClass(false, "public sealed", converterName, $"ValueConverter<{data.ClassName}, {data.PrimitiveTypeFriendlyName}>"); + + builder.AppendSummary($"Constructor to create {converterName}") + .AppendLine($"public {converterName}() : base(v=> v, v=> v)") + .OpenBracket() + .CloseBracket(); + + builder.CloseBracket(); + + context.AddSource($"{converterName}.g.cs", builder.ToString()); + } + /// /// Generates code for a JsonConverter for the specified type. /// diff --git a/src/AltaSoft.DomainPrimitives.Generator/Models/DomainPrimitiveGlobalOptions.cs b/src/AltaSoft.DomainPrimitives.Generator/Models/DomainPrimitiveGlobalOptions.cs index d71e4cf..6f6ef6c 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Models/DomainPrimitiveGlobalOptions.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Models/DomainPrimitiveGlobalOptions.cs @@ -40,4 +40,12 @@ internal sealed record DomainPrimitiveGlobalOptions /// true if XML serialization methods should be generated; otherwise, false. /// public bool GenerateXmlSerialization { get; set; } -} \ No newline at end of file + + /// + /// Gets or sets a value indicating whether Entity Framework Core value converters should be generated for Domain Primitive types. + /// + /// + /// true if Entity Framework Core value converters should be generated; otherwise, false. + /// + public bool GenerateEntityFrameworkCoreValueConverters { get; set; } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelpers.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelpers.cs index db1ca96..aa06bb6 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelpers.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelpers.cs @@ -1,9 +1,9 @@ -using AltaSoft.DomainPrimitives.Generator.Models; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using AltaSoft.DomainPrimitives.Generator.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; // ReSharper disable ConvertToPrimaryConstructor @@ -93,6 +93,10 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val value = _options.GenerateTypeConverters.ToString(); return true; + case "build_property.DomainPrimitiveGenerator_GenerateEntityFrameworkCoreValueConverters": + value = _options.GenerateEntityFrameworkCoreValueConverters.ToString(); + return true; + default: value = null; return false; From 9abfc75b65e48f76e48129e4b9c1af1c1a9a0441 Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Tue, 18 Jun 2024 17:54:28 +0400 Subject: [PATCH 2/2] Updated readme and added an example makrdown file for EntityFrameworkCore Value Converters --- Directory.Build.props | 2 +- EntityFrameworkCoreExample.md | 126 ++++++++++++++++++++++++++++++++++ README.md | 1 + 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 EntityFrameworkCoreExample.md diff --git a/Directory.Build.props b/Directory.Build.props index 0755370..c7047a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ Domain Primitives ALTA Software llc. Copyright © 2024 ALTA Software llc. - 4.0.0 + 4.1.0 diff --git a/EntityFrameworkCoreExample.md b/EntityFrameworkCoreExample.md new file mode 100644 index 0000000..d0a93e8 --- /dev/null +++ b/EntityFrameworkCoreExample.md @@ -0,0 +1,126 @@ + +# Generating EntityFrameworkCore Value Converters + +1. In the `.csproj` file where **AltaSoft.DomainPrimitives.Generator** is located, add the following item: + +```xml + + true + +``` + +**Note:** Ensure EntityFrameworkCore is added in the references. + +After this, ValueConverters for each DomainPrimitive will be generated. + +## Example +Given a domain primitive `AsciiString`: + +```csharp +/// +/// A domain primitive type representing an ASCII string. +/// +/// +/// The AsciiString ensures that its value contains only ASCII characters. +/// +public partial class AsciiString : IDomainValue +{ + /// + public static PrimitiveValidationResult Validate(string value) + { + var input = value.AsSpan(); + + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < input.Length; i++) + { + if (!char.IsAscii(input[i])) + return "value contains non-ascii characters"; + } + + return PrimitiveValidationResult.Ok; + } +} +``` + +The following converter will be generated: + +```csharp +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives.XmlDataTypes; +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using AltaSoft.DomainPrimitives; + +namespace AltaSoft.DomainPrimitives.XmlDataTypes.EntityFrameworkCore.Converters; + +/// +/// ValueConverter for +/// +public sealed class AsciiStringValueConverter : ValueConverter +{ + /// + /// Constructor to create AsciiStringValueConverter + /// + public AsciiStringValueConverter() : base(v => v, v => v) + { + } +} +``` + +**Note:** All Domain Primitives have implicit conversion to/from their primitive type. Therefore, no explicit conversion is required. + +## Helper Extension Method + +A helper extension method is also generated to add domain primitive conversions globally to `ModelConfigurationBuilder`: + +```csharp +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives.XmlDataTypes; +using Microsoft.EntityFrameworkCore; +using AltaSoft.DomainPrimitives.XmlDataTypes.EntityFrameworkCore.Converters; + +namespace AltaSoft.DomainPrimitives.XmlDataTypes.Converters.Extensions; + +/// +/// Helper class providing methods to configure EntityFrameworkCore ValueConverters for DomainPrimitive types of AltaSoft.DomainPrimitives.XmlDataTypes +/// +public static class ModelConfigurationBuilderExt +{ + /// + /// Adds EntityFrameworkCore ValueConverters for specific custom types to ensure proper mapping to EFCore ORM. + /// + /// The ModelConfigurationBuilder instance to which converters are added. + public static ModelConfigurationBuilder AddDomainPrimitivePropertyConversions(this ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + return configurationBuilder; + } +} +``` diff --git a/README.md b/README.md index 2c3888e..ccd89a1 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ The **AltaSoft.DomainPrimitives.Generator** offers a diverse set of features: * **NumberType Operations:** Automatically generates basic arithmetic and comparison operators, by implementing Static abstract interfaces. [More details regarding numeric types](#number-types-attribute) * **IParsable Implementation:** Automatically generates parsing for non-string types. * **XML Serialiaziton** Generates IXmlSerializable interface implementation, to serialize and deserialize from/to xml. +* **EntityFrameworkCore ValueConverters** Facilitates seamless integration with EntityFrameworkCore by using ValueConverters to treat the primitive type as its underlying type. For more details, refer to [EntityFrameworkCore ValueConverters](EntityFrameworkCoreExample.md) ## Supported Underlying types 1. `string`