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.
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, ifT
is astring
, thenT?
is astring?
.- If the type argument for
T
is a value type,T?
references the same value type,T
. For example, ifT
is anint
, theT?
is also anint
.- If the type argument for
T
is a nullable reference type,T?
references that same nullable reference type. For example, ifT
is astring?
, thenT?
is also astring?
.- If the type argument for
T
is a nullable value type,T?
references that same nullable value type. For example, ifT
is aint?
, thenT?
is also aint?
.
I haven't tested it, but changing Guid
to Guid?
while keeping the IsRequired()
method on CreatedById
property relationship might solve your problem.
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; }
...
}