Skip to content

Commit

Permalink
Merge pull request #13 from altasoft/feature/EFValueConverter
Browse files Browse the repository at this point in the history
Added support for EntityFrameworkCore ValueConverters
  • Loading branch information
GregoryNikolaishvili authored Jun 18, 2024
2 parents 27392d5 + 9abfc75 commit 4398111
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Product>Domain Primitives</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>4.0.0</Version>
<Version>4.1.0</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
126 changes: 126 additions & 0 deletions EntityFrameworkCoreExample.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@

# Generating EntityFrameworkCore Value Converters

1. In the `.csproj` file where **AltaSoft.DomainPrimitives.Generator** is located, add the following item:

```xml
<PropertyGroup>
<DomainPrimitiveGenerator_GenerateEntityFrameworkCoreValueConverters>true</DomainPrimitiveGenerator_GenerateEntityFrameworkCoreValueConverters>
</PropertyGroup>
```

**Note:** Ensure EntityFrameworkCore is added in the references.

After this, ValueConverters for each DomainPrimitive will be generated.

## Example
Given a domain primitive `AsciiString`:

```csharp
/// <summary>
/// A domain primitive type representing an ASCII string.
/// </summary>
/// <remarks>
/// The AsciiString ensures that its value contains only ASCII characters.
/// </remarks>
public partial class AsciiString : IDomainValue<string>
{
/// <inheritdoc/>
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
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
#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;

/// <summary>
/// ValueConverter for <see cref="AsciiString"/>
/// </summary>
public sealed class AsciiStringValueConverter : ValueConverter<AsciiString, string>
{
/// <summary>
/// Constructor to create AsciiStringValueConverter
/// </summary>
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
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable

using AltaSoft.DomainPrimitives.XmlDataTypes;
using Microsoft.EntityFrameworkCore;
using AltaSoft.DomainPrimitives.XmlDataTypes.EntityFrameworkCore.Converters;

namespace AltaSoft.DomainPrimitives.XmlDataTypes.Converters.Extensions;

/// <summary>
/// Helper class providing methods to configure EntityFrameworkCore ValueConverters for DomainPrimitive types of AltaSoft.DomainPrimitives.XmlDataTypes
/// </summary>
public static class ModelConfigurationBuilderExt
{
/// <summary>
/// Adds EntityFrameworkCore ValueConverters for specific custom types to ensure proper mapping to EFCore ORM.
/// </summary>
/// <param name="configurationBuilder">The ModelConfigurationBuilder instance to which converters are added.</param>
public static ModelConfigurationBuilder AddDomainPrimitivePropertyConversions(this ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<AsciiString>().HaveConversion<AsciiStringValueConverter>();
configurationBuilder.Properties<GDay>().HaveConversion<GDayValueConverter>();
configurationBuilder.Properties<GMonth>().HaveConversion<GMonthValueConverter>();
configurationBuilder.Properties<GMonthDay>().HaveConversion<GMonthDayValueConverter>();
configurationBuilder.Properties<GYear>().HaveConversion<GYearValueConverter>();
configurationBuilder.Properties<GYearMonth>().HaveConversion<GYearMonthValueConverter>();
configurationBuilder.Properties<NegativeInteger>().HaveConversion<NegativeIntegerValueConverter>();
configurationBuilder.Properties<NonEmptyString>().HaveConversion<NonEmptyStringValueConverter>();
configurationBuilder.Properties<NonNegativeInteger>().HaveConversion<NonNegativeIntegerValueConverter>();
configurationBuilder.Properties<NonPositiveInteger>().HaveConversion<NonPositiveIntegerValueConverter>();
configurationBuilder.Properties<PositiveInteger>().HaveConversion<PositiveIntegerValueConverter>();
return configurationBuilder;
}
}
```
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<CompilerVisibleProperty Include="DomainPrimitiveGenerator_GenerateTypeConverters" />
<CompilerVisibleProperty Include="DomainPrimitiveGenerator_GenerateSwaggerConverters" />
<CompilerVisibleProperty Include="DomainPrimitiveGenerator_GenerateXmlSerialization" />
<CompilerVisibleProperty Include="DomainPrimitiveGenerator_GenerateEntityFrameworkCoreValueConverters" />
</ItemGroup>

<!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
11 changes: 11 additions & 0 deletions src/AltaSoft.DomainPrimitives.Generator/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal static void Execute(
return;

var swaggerTypes = new List<GeneratorData>(typesToGenerate.Length);
var efCoreValueConverterTypes = new List<INamedTypeSymbol>(typesToGenerate.Length);
var cachedOperationsAttributes = new Dictionary<INamedTypeSymbol, SupportedOperationsAttributeData>(SymbolEqualityComparer.Default);

try
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,92 @@ internal static void ProcessTypeConverter(GeneratorData data, SourceProductionCo
context.AddSource($"{data.ClassName}TypeConverter.g.cs", builder.ToString());
}

/// <summary>
/// Generates the value converters extension for the specified assembly name, types, and source production context.
/// </summary>
/// <param name="addAssemblyAttribute">if assembly attribute should be added</param>
/// <param name="assemblyName">The name of the assembly.</param>
/// <param name="types">The list of named type symbols.</param>
/// <param name="context">The source production context.</param>
internal static void GenerateValueConvertersExtension(bool addAssemblyAttribute, string assemblyName, List<INamedTypeSymbol> 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());
}

/// <summary>
/// Processes the Entity Framework value converter for the specified generator data and source production context.
/// </summary>
/// <param name="data">The generator data.</param>
/// <param name="context">The source production context.</param>
internal static void ProcessEntityFrameworkValueConverter(GeneratorData data, SourceProductionContext context)
{
var builder = new SourceCodeBuilder();

builder.AppendSourceHeader("AltaSoft DomainPrimitives Generator");

var usingStatements =
new List<string>(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 <see cref = \"{data.ClassName}\"/>");
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());
}

/// <summary>
/// Generates code for a JsonConverter for the specified type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ internal sealed record DomainPrimitiveGlobalOptions
/// <c>true</c> if XML serialization methods should be generated; otherwise, <c>false</c>.
/// </value>
public bool GenerateXmlSerialization { get; set; }
}

/// <summary>
/// Gets or sets a value indicating whether Entity Framework Core value converters should be generated for Domain Primitive types.
/// </summary>
/// <value>
/// <c>true</c> if Entity Framework Core value converters should be generated; otherwise, <c>false</c>.
/// </value>
public bool GenerateEntityFrameworkCoreValueConverters { get; set; }
}
10 changes: 7 additions & 3 deletions tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 4398111

Please sign in to comment.