SteveDunn / Vogen

A semi-opinionated library which is a source generator and a code analyser. It Source generates Value Objects
Apache License 2.0
888 stars 46 forks source link

EF Converters in Separate Project #676

Closed ardalis closed 1 month ago

ardalis commented 1 month ago

Describe the feature

I'm trying to generate a VO for a Name in my Core project which has no references to EF Core. However, the generated code has errors at compile time, saying image

This issue is that I don't have EF Core referenced from this project, nor do I want to do so. That is only referenced from my Infrastructure project, which is also where my DbContext and all of my EF configuration classes are.

Is there a way to generate the converters in the Infrastructure project rather than in the VO itself?

My Value Object (in Core project)

[ValueObject<string>(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson)]
public partial class ProjectName
{
  private static Validation Validate(in string name) => String.IsNullOrEmpty(name) ? 
    Validation.Invalid("Name cannot be empty") : 
    Validation.Ok;
}
ardalis commented 1 month ago

If nothing else, I wonder if we could have something in a separate project that would work like this:

[ValueConverter<ProjectName>(Converters.EFCore)]
public partial ProjectNameConverter();

You could define separate ones (in separate projects if desired) for different converters, or use the | syntax to put several into one class.

How easy/hard would this be?

ardalis commented 1 month ago

Ok as a workaround I have this working:


// in Core project - note no reference to EF Core here and I removed all references from the .csproj

[ValueObject<string>(conversions: Conversions.SystemTextJson)]
public partial class ProjectName
{
  private static Validation Validate(in string name) => String.IsNullOrEmpty(name) ? 
    Validation.Invalid("Name cannot be empty") : 
    Validation.Ok;
}

// in Infrastructure project with DbContext (and EF Core package references)

using NimblePros.SampleToDo.Core.ProjectAggregate;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Vogen;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace NimblePros.SampleToDo.Infrastructure.Data.Config;

public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
  public void Configure(EntityTypeBuilder<Project> builder)
  {
    builder.Property(p => p.Name)
      //.HasVogenConversion()
      .HasConversion(new EfCoreValueConverter(), new EfCoreValueComparer())
      .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH)
      .IsRequired();

    builder.Property(p => p.Priority)
      .HasConversion(
          p => p.Value,
          p => Priority.FromValue(p));
  }

  public class EfCoreValueConverter : ValueConverter<ProjectName, String>
  {
    public EfCoreValueConverter() : this(null)
    {
    }

    public EfCoreValueConverter(ConverterMappingHints mappingHints = null)
      : base(vo => vo.Value, value => ProjectName.From(value), mappingHints)
    {
    }
  }

  public class EfCoreValueComparer : ValueComparer<ProjectName>
  {
    public EfCoreValueComparer() : base((left, right) => DoCompare(left, right), 
      instance => instance.IsInitialized() ? instance.Value.GetHashCode() : 0)
    {
    }

    static bool DoCompare(ProjectName left, ProjectName right)
    {
      // if both null, then they're equal
      if (left is null)
        return right is null;
      // if only right is null, then they're not equal
      if (right is null)
        return false;
      // if they're both the same reference, then they're equal
      if (ReferenceEquals(left, right))
        return true;
      // if neither are initialized, then they're equal
      if (!left.IsInitialized() && !right.IsInitialized())
        return true;
      return left.IsInitialized() && right.IsInitialized() && left.Value.Equals(right.Value);
    }
  }

}

Basically I looked at the generated code and copied it out into my Configuration class. I had to change _value to Value in a few places and a call to _Deserialize(string) to just From(string) but otherwise it was pretty simple. And then I specified the types directly instead of having the handy Vogen helper:

      //.HasVogenConversion()
      .HasConversion(new EfCoreValueConverter(), new EfCoreValueComparer())

So, it works this way, but this will result in lots of duplicate code, which is what Vogen is great at. I'm hoping someone can move/copy the code generation for EF Core converter/comparer so that it can be applied to a class in a separate project (and maybe adjust HasVogenConversion to be able to discover it as well).

Thanks!

SteveDunn commented 1 month ago

Hi @ardalis - yes, this is possible and is documented here: https://stevedunn.github.io/Vogen/efcoreintegrationhowto.html Hopefully that'll work for you, but please let me know if there's any issues or if the process could be improved.

SteveDunn commented 1 month ago

Closing, but do let me know if you have any further questions if this isn't resolved.

ardalis commented 1 month ago

No this is perfect and I'm glad you'd already thought to do it. I just didn't see it in the docs (I did look...). Thanks!