mcintyre321 / ValueOf

Deal with Primitive Obsession - define ValueObjects in a single line (of C#).
MIT License
861 stars 39 forks source link

Using ValueOf with Entity Framework Core? #28

Open jyscao opened 2 years ago

jyscao commented 2 years ago

Hi, I'm trying to incorporate your excellent library into my EFCore model classes, for example:

    public partial class Account
    {
        public Account()
        {
            WorkerGroups = new HashSet<WorkerGroup>();
        }

        public LongId Id { get; set; }
        public DateTime? Created { get; set; }
        public DateTime? AgreedToTermsOn { get; set; }
        public NameLabel Name { get; set; }
        public GuidStr ApiKey { get; set; }
        public string CustomUrl { get; set; }

        public virtual ICollection<WorkerGroup> WorkerGroups { get; set; }
    }

Where LongId, NameLabel, GuidStr have C# primitive types of long, string, string respectively, with appropriate validation checks (e.g. under a certain length for NameLabel, and of certain format for GuidStr).

The code compiles fine, however when I actually try to fetch one of the above record from the database, I'm running into this error: System.InvalidOperationException: The property 'Account.Id' is of type 'LongId' which is not supported by the current database provider. Either change the property CLR type, or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

I also tried to annotate the model with something like [Column(TypeName = "bigint")] on the Id property for example, and it didn't do anything. So I just wanted to ask you if what I'm trying to do is reasonable and/or feasible. Maybe I should just return to using primitive C# types?

chucker commented 2 years ago

I'm not that experienced with EF, but maybe this will work?

[Column(TypeName = "int, not null")] // explicitly map the SQL data type to `int`
public LongId Id { get; set; }

Failing something like that, you're probably better off using something like AutoMapper to separate your entities (classes that map 1:1 to fields in your database and are only used in code that directly interacts with the DB) and your models (classes that use ValueOf).

SteveDunn commented 2 years ago

Vogen is similar to this but it has certain opinions/constraints on safety (for instance, not being able to create default instances of value objects). It's also focused on speed; there's almost no overheard compared to using a primitive directly. It's a source generator, and can generate code for EF (as well as Json and Dapper). I'm not after stealing customers away from this very useful little package, but you might find it has what you need (if you're happy with the constraints it imposes)

jyscao commented 2 years ago

@chucker tried your suggestion just now, same error as before. In fact even when I annotate the property with [NotMapped], the same error still pops up. So I get the feeling that I haven't even been addressing the issue at the right level. To me EFCore definitely contains enough magic that I'm not even sure where to really start looking into this. Thanks anyway for trying to help.

jyscao commented 2 years ago

@SteveDunn thanks for the tip, for the time being I've switched back to working in primitive obsession mode just because I want to make progress on the actual code I gotta get done (it's for work unfortunately). But I'll keep your package in mind for when I do the refactoring down the line, hopefully sooner rather than later.

mcintyre321 commented 2 years ago

I haven't really used EF for a while, but I suspect you need a custom ValueConverter https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations

Another option might be https://docs.microsoft.com/en-us/ef/core/modeling/backing-field?tabs=data-annotations

ttlaare commented 2 years ago

Using value conversions with ValueOf works perfectly fine.

public class CustomerContext : DbContext
{
    public CustomerContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Customer>()
            .Property(e => e.Id)
            .HasConversion(
                convertToProviderExpression: id => id.Value,
                convertFromProviderExpression: id => CustomerId.From(id));

        modelBuilder
            .Entity<Customer>()
            .Property(e => e.Email)
            .HasConversion(
                convertToProviderExpression: emailAddress => emailAddress.Value,
                convertFromProviderExpression: emailAddress => EmailAddress.From(emailAddress));
    }
}

Check out my repository for a fully working example: https://github.com/ttlaare/ValueOfWithEf

jlauwers commented 2 years ago

If you want a generic ValueOfConverter:

public class ValueOfConverter<T, TType> : ValueConverter<T, TType>
    where T : ValueOf<TType, T>, new()
{
    public ValueOfConverter() : base(
        id => id.Value,
        id => ValueOf<TType, T>.From(id))
    {
    }
}

builder.Property(x => x.Foobar).HasConversion<ValueOfConverter<Foobar, int>>();