Search code examples
c#entity-frameworkfluent

Fluent API Marking Abstract Nullable Field as Required


I'm working on implementing a library to enforce the Auditable Entity design pattern, aiming for easy implementation across projects for both Entities and their configuration. Here's my current setup:

public interface IAuditable<T, TId>
{
    DateTime CreatedOn { get; set; }
    DateTime? UpdatedOn { get; set; }
    TId CreatedById { get; set; }
    T CreatedBy { get; set; }
    TId? UpdatedById { get; set; }
    T? UpdatedBy { get; set; }
}

public abstract class AuditableEntity<T, TId> : IAuditable<T, TId>
{
    public DateTime CreatedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public TId CreatedById { get; set; }
    public T CreatedBy { get; set; }
    public TId? UpdatedById { get; set; }
    public T? UpdatedBy { get; set; }
}

public abstract class AuditableEntityTypeConfiguration<T, TId, Th> : IEntityTypeConfiguration<Th>
    where Th : AuditableEntity<T, TId> 
    where T : class
{
    public virtual void Configure(EntityTypeBuilder<Th> builder)
    {
        builder.Property(e => e.CreatedOn).IsRequired();
        builder.Property(e => e.UpdatedOn);
    
        builder.HasOne(x => x.CreatedBy)
        .WithMany()
        .HasForeignKey(x => x.CreatedById)
        .IsRequired();
    
        builder.HasOne(x => x.UpdatedBy)
        .WithMany()
        .HasForeignKey(x => x.UpdatedById);
    }
}

public class Paper : AuditableEntity<User, Guid>
{
    public Guid Id { get; set; }
    ...

}

However, when I attempt to make a migration, it always returns changes making the previously nullable property UpdatedById to now be required.

I've tried adding this to my OnModelCreating in addition to implementing the abstract class as AuditableEntity<User, Guid?> in my DbContext:

modelBuilder.Entity<Paper>().Property(p => p.UpdatedById).IsRequired(false); 
modelBuilder.Entity<Subject>().Property(p => p.UpdatedById).IsRequired(false);

Upon running the migration, I am left with:

Unable to create a 'DbContext' of type '{MyDbContext}'. The exception 'The property 'Paper.UpdatedById' cannot be marked as nullable/optional because the type of the property is 'Guid' which is not a nullable type. Any property can be marked as non-nullable/required, but only properties of nullable types can be marked as nullable/optional.' was thrown while attempting to create an instance. For the different patterns supported at design time, see here.

Does anyone have any ideas on how to resolve this?

EDIT: I have found a work around specific for value types being passed as nullable. I had to update my IAuditable interface to be like so:

public interface IAudtiable<T, TId> where TId : struct
{
...
Nullable<TId> UpdatedById {get; set;}
...
}

However, this now disallows the use of strings as ids and overall puts pressure on the user's implementation to have the mutating entities Id type be a value type, which seems to be outside of the scope of this library in my opinion.


Solution

  • The problem is in the type you pass as a generic argument. Since Guid is a value type, the T? doesn't actually work in the way you expect.

    Rules for nullable types in generics are explained in the docs:

    • If the type argument for T is a reference type, T? references the corresponding nullable reference type. For example, if T is a string, then T? is a string?.
    • If the type argument for T is a value type, T? references the same value type, T. For example, if T is an int, the T? is also an int.
    • If the type argument for T is a nullable reference type, T? references that same nullable reference type. For example, if T is a string?, then T? is also a string?.
    • If the type argument for T is a nullable value type, T? references that same nullable value type. For example, if T is a int?, then T? is also a int?.

    I haven't tested it, but changing Guid to Guid? while keeping the IsRequired() method on CreatedById property relationship might solve your problem.

    Update

    In case you want to keep the CreatedById non-null, you can change your interface to explicitly take the nullable value type, then add another interface implementation with two generic types for reference types only:

    public interface IAuditable<T, TId, TNullableId>
    {
        DateTime CreatedOn { get; set; }
        DateTime? UpdatedOn { get; set; }
        TId CreatedById { get; set; }
        T CreatedBy { get; set; }
        TNullableId? UpdatedById { get; set; }
        T? UpdatedBy { get; set; }
    }
    
    public abstract class AuditableEntity<T, TId, TNullableId> : IAuditable<T, TId, TNullableId>
    {
        public DateTime CreatedOn { get; set; }
        public DateTime? UpdatedOn { get; set; }
        public TId CreatedById { get; set; }
        public T CreatedBy { get; set; }
        public TNullableId? UpdatedById { get; set; }
        public T? UpdatedBy { get; set; }
    }
    
    public abstract class AuditableEntity<T, TId> : AuditableEntity<T, TId, TId>
        where TId : class
    {
    }
    

    And then to use it with value types:

    public class Paper : AuditableEntity<User, Guid, Guid?>
    {
        public Guid Id { get; set; }
        ...
    
    }
    

    And to use it with reference types:

    public class Paper : AuditableEntity<User, string>
    {
        public string Id { get; set; }
        ...
    
    }