Search code examples
c#.net-core

How to create a generic interface that allows nullable get properties in .NET 8 C#


I am trying to create a generic interface to be applied to class objects that have to be auditable: i.e. have the ID of the user that created, modified, or deleted the object, and the DateTime of the event.

The problem is that an object that has not been modified or deleted must have a null value for the corresponding Id and Date values.

My current definition is:

public interface IAuditedEntity<UserIdType>
{
    UserIdType CreatedBy { get; }
    DateTime DateCreated { get; }
    UserIdType? ModifiedBy { get; }
    DateTime? DateModified { get; }
    UserIdType? DeletedBy { get; }
    DateTime? DateDeleted { get; }
}

public class AuditedClass : IAuditedEntity<int>
{
    public int Id { get; protected set; }
    public int CreatedBy { get; protected set; }
    public DateTime DateCreated { get; set; }
    public int? ModifiedBy { get; protected set; }
    public DateTime? DateModified {get; protected set; }
    public int? DeletedBy { get; protected set; }
    public DateTime? DateDeleted { get; protected set; }
}

In Visual Studio 2022 (latest release), I get an error that the interface is not completely implemented.

I need the generic because in this module the user's Id is an int, but in other modules it could be a string or GUID.


Solution

  • The problem here is that you're getting confused by the syntax you're using. T1? and T2? aren't the same generic type behind the scenes with different specializations, because for example T1 might be an int (which makes the first type Nullable<int>) and T2 might be string (which makes the second type String, not Nullable<String>).

    In other words, the little question mark means different things depending on the type of the object you're attaching it to, and it specifically means nothing (to the CLR) when attached to a reference type.

    To get your example to compile, the fix is trivial, simply specify in which of the two cases you fall:

    interface IAuditedEntity<UserIdType> where UserIdType: struct
    

    The rest of the code will compile as-is.

    The same will work for Guid (it's a value type), but not for string (it's a reference type). A string won't bind to that interface, you'll have to make a different one for reference types to use.