4
votes

I've written two abstract classes that represent the base class for entities: One where the Id property is an int, another that allows to specify the type of the Id property using a generic type parameter TId:

/// <summary>
///     Represents the base class for all entities.
/// </summary>
[System.Serializable]
public abstract class BaseEntity
{
    /// <summary>
    ///     Gets or sets the ID of the entity.
    /// </summary>
    public int Id { get; set; }
}

/// <summary>
///     Represents the base class for all entities that have an ID of type <typeparamref name="TId"/>.
/// </summary>
/// <typeparam name="TId">
///     The type of the <see cref="Id"/> property.
/// </typeparam>
[System.Serializable]
public abstract class BaseEntity<TId>
{
    /// <summary>
    ///     Gets or sets the ID of the entity.
    /// </summary>
    public TId Id { get; set; }
}

These classes are defined in a core assembly which I use in nearly all projects I work on. Since C# 8.0 came out, I've experimented with enabling nullable reference types, which has worked out well so far.

However, in the case of BaseEntity<TId>, the compiler gives a warning:

Non-nullable property 'Id' is uninitialized. Consider declaring the property as nullable.

I understand the warning, but I can't seem to fix the issue for my use case. More specifically, I want to allow declaration of types that derive from:

  • System.String, i.e. BaseEntity<string>
  • Any value type, e.g. BaseEntity<System.Guid> or a custom struct

Since System.String isn't a value type, this doesn't seem to be possible: If I constrain TId to structs (BaseEntity<TId> where TId : struct), I can't declare BaseEntity<string> any more.

The only solution (?) I've found so far to disable the warning, is initializing the Id property with its default value and using the ! operator:

/// <summary>
///     Represents the base class for all entities that have an ID of type <typeparamref name="TId"/>.
/// </summary>
/// <typeparam name="TId">
///     The type of the <see cref="Id"/> property.
/// </typeparam>
[System.Serializable]
public abstract class BaseEntity<TId>
{
    /// <summary>
    ///     Gets or sets the ID of the entity.
    /// </summary>
    public TId Id { get; set; } = default!;
}

However, I'd like to make the intent of the code clear: That TId can be a value type (e.g. short, long, System.Guid, ...), OR a System.String.

Is this somehow possible?

1
By the way, you may want to disallow nullable reference for TId, you cans use where TId : notnull to prohibit class Entity : BaseEntity<string?> usages.Orace

1 Answers

3
votes

No, there's no such constraint - whether you're using nullable reference types or not.

What you could potentially do is use a private constructor to make sure that only types declared within the base type can derive from BaseEntity, and then use two specific versions:

public abstract class BaseEntity<TId>
{
    public TId Id { get; set; }

    private BaseEntity<TId>(Id id) => Id = id;

    public class StructEntity<T> : BaseEntity<T> where T : struct
    {
        public StructEntity() : base(default) {}
    }

    public class StringEntity : BaseEntity<string>
    {
        public StringEntity(string id) : base(id) {}
    }
}

That would still let you work with BaseEntity<T> in most places, but any time you wanted to construct an entity, you'd need to pick between those two.

I've no idea how that will tie in with supporting serialization, although I'd personally steer clear of binary serialization anyway.