andrewlock / StronglyTypedId

A Rosyln-powered generator for strongly-typed IDs
MIT License
1.52k stars 79 forks source link

Nullable support in EF Core #22

Closed kfrancis closed 3 years ago

kfrancis commented 4 years ago

What's the best way to add nullable support here? You discuss it in previous posts, but it doesn't seem to be part of the library.

The property 'Claim.CaseId' could not be mapped, because it is of type 'Nullable' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'

CaseId is defined:

[StronglyTypedId(jsonConverter: StronglyTypedIdJsonConverter.SystemTextJson | StronglyTypedIdJsonConverter.NewtonsoftJson, backingType: StronglyTypedIdBackingType.Guid)]
    public partial struct CaseId { }

And used:

public class Claim {
    public CaseId? CaseId { get; set; }
}

I have also:

public class CaseIdValueConverter : ValueConverter<CaseId, Guid>
    {
        public CaseIdValueConverter(ConverterMappingHints mappingHints = null)
            : base(
                id => id.Value,
                value => new CaseId(value),
                mappingHints
            )
        { }
    }

and ReplaceService<IValueConverterSelector, StronglyTypedIdValueConverterSelector>()

patrolez commented 4 years ago

@kfrancis: https://stackoverflow.com/a/4491882/12755962

This suggests that no struct can be Nullable and a proposed solution is to use class instead.

In case of project's source code, at least there is a constraint about a stuct: https://github.com/andrewlock/StronglyTypedId/blob/4b533c1552cc06d11b12f6cd8843cc3a089dc54d/src/StronglyTypedId.Attributes/StronglyTypedIdAttribute.cs#L5

And in a following file for code generator (and its sibling files): https://github.com/andrewlock/StronglyTypedId/blob/4b533c1552cc06d11b12f6cd8843cc3a089dc54d/src/StronglyTypedId.Generator/templates/GuidId.cs#L4

And it might be not the only thing as I am not familiar what constraints are put on auto-magically (de)serializable by Json frameworks types. Maybe to be a struct is essential.

@andrewlock

andrewlock commented 4 years ago

This suggests that no struct can be Nullable and a proposed solution is to use class instead

@patrolez You're getting confused between 2 concepts here. A class can be null naturally, and you're right that a struct can't ever be null. However, you can make a Struct nullable using the ? symbol, e.g. CaseId can be null below.

public class Claim {
    public CaseId? CaseId { get; set; }
}

As to the original question, this appears to be a problem with the value converters. I don't have a specific answer as it's not something I've ever tried to do. It's essentially an EF Core problem rather than this lib, but I don't have a solution off-hand, at this point

vebbo2 commented 4 years ago

@kfrancis This is the way I'm using StronglyTypedId in EF Core, works fine with nullables

public static class ModelBuilderExtensions
    {
        public static void ApplyStronglyTypedIdConversion(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                foreach (var property in entityType.GetProperties())
                {
                    if (property.ClrType.GetCustomAttribute<StronglyTypedIdAttribute>() != null)
                    {
                        var converterType = typeof(StronglyTypedIdValueConverter<>).MakeGenericType(property.ClrType);
                        var converter = (ValueConverter)Activator.CreateInstance(converterType);
                        property.SetValueConverter(converter);
                    }
                }
            }
        }
    }

and

public class StronglyTypedIdValueConverter<TId> : ValueConverter<TId, Guid>
    {
        public StronglyTypedIdValueConverter()
            : base(e => (Guid)e.GetType().GetProperty("Value").GetValue(e), e => (TId)Activator.CreateInstance(typeof(TId), e))
        {
        }
    }

PS. This code assumes you always use same backing type, in this case Guid

dzmitry-lahoda commented 4 years ago

@vebbo2 PR to handle all ids in EF?

vebbo2 commented 4 years ago

@dzmitry-lahoda It would probably require separate package, core library definietely shouldn't depend on EF Core

zyberzero commented 4 years ago

@vebbo2 : Thanks for that! I borrowed your example. However, I can't get it to run - the problem is when looping over all properties in the entityType the property containing the strongly typed Id type is missing. For example, I have this entity:

    public class Option
    {
        public OptionId ID { get; set; }
        public string AnswerText { get; set; }
    }

Where OptionId is defined as:

    [StronglyTypedId]
    public partial struct OptionId { }

I get two properties for the entityType; AnswerType (string) and PollTempId (int32) - I guess that's due to the fact that the Option is owned by a Poll type (which also as ha StronglyTypedId as its key) - but I can't find any reference to the OptionId at all. Do you have any idea what I'm doing wrong?

vebbo2 commented 4 years ago

@zyberzero I didn't handle owned types in my case, so no wonder it's not working :) But what comes to my mind right now is, while iterating over properties, just check if they are classes and then iterate over its properties for STI attribute as well. It's not a perfect solution but it might work for you

zyberzero commented 4 years ago

@vebbo2 : After thinking about it, it does not find any StronglyTypedIds, even though they are the root object.

My DbContext has a DbSet. It looks, somewhat minified as:

 public class Poll
    {
        public PollId Id { get; set; }
        public IList<Option> Options { get; set; }
    }

However, the Properties on that Entity are as follows:

entity.GetProperties()
Count = 1
    [0]: {Property: Poll.TempId (no field, int) Shadow Required AlternateKey AfterSave:Throw}

So it seems to me that EF does not want to see my StronglyTypedIds at all. I think I'm doing something stupid but I can't really figure out what.

But you maybe didn't used the StronglyTypedIds as the key either?

Is your code available so that I can take a look in how you did, and figure out what differs between us?

vebbo2 commented 4 years ago

@zyberzero Sorry, it's private project, but I remember having similar issue. If your STIs are in different assembly, they may not visible because STI is marked with https://github.com/andrewlock/StronglyTypedId/blob/master/src/StronglyTypedId.Attributes/StronglyTypedIdAttribute.cs#L7 . To workaround this, add CodeGeneration to Conditional compilation symbols in your project

andrewlock commented 4 years ago

@zyberzero Just to jump in, but this doesn't look like a problem with StronglyTYpedIds to me, it looks like a problem with your EF Core mapping. For example, EF Core isn't recognising the Options property on your Poll entity at all - the fact that Option.ID is a strongly typed ID isn't a factor at that point I don't think.

That said, I don't do a lot of EF Core, so I could be wrong, but does it work if you switch Option.Id to being a simple Guid/int? That would help narrow things down

andrewlock commented 3 years ago

I've released a beta version of a major update to the library converts to using Source generators. There's quite a few breaking changes in the release, so please make sure to check the release notes for how to update, but I'd appreciate any feedback you have before I do a final release! The release adds direct support for ef core value converters.

I've also written a blog post announcing the update that goes into a bit more details: https://andrewlock.net/rebuilding-stongly-typed-id-as-a-source-generator-1-0-0-beta-release

Thanks! 🙂

kfrancis commented 3 years ago

Perfect, thanks!