DEV_NET_CORE
GET_STARTED
.NETModern C# patterns

Generic Type Constraints and Reusable Components

Overview

Generic type constraints in C# define what kind of type can be used as a generic type argument. They are written with the where keyword and allow a generic class, method, interface, delegate, or struct to safely use members or behaviors that are guaranteed to exist on the type parameter.

Generics help developers write reusable, type-safe components without duplicating code for every concrete type. Constraints make those reusable components more precise. Instead of accepting "any type", a generic component can require a reference type, value type, interface implementation, base class, public parameterless constructor, enum, delegate, unmanaged type, or another generic type relationship.

This topic matters because generics appear everywhere in real C# and .NET development:

  • List<T>, Dictionary<TKey, TValue>, IEnumerable<T>, and LINQ
  • Repository and service abstractions
  • CQRS commands, queries, handlers, validators, and pipeline behaviors
  • Result wrappers such as Result<T>
  • Mapping, caching, serialization, and factory components
  • Generic math and reusable algorithms
  • Strongly typed IDs and domain models

For interviews, generic constraints are important because they test whether a developer understands type safety, reusable design, compile-time guarantees, inheritance, interfaces, variance, nullable reference types, and the trade-offs between abstraction and complexity. A strong candidate should know not only the syntax, but also when constraints improve a design and when they make code unnecessarily complicated.

Core Concepts

What Generics Solve

Generics allow code to work with a type parameter instead of a specific type.

Without generics, reusable code often uses object, which loses type safety and can require casts:

Code
public class ObjectBox
{
    public object Value { get; set; }

    public ObjectBox(object value)
    {
        Value = value;
    }
}

var box = new ObjectBox("hello");
string text = (string)box.Value;

With generics, the type is known at compile time:

Code
public class Box<T>
{
    public T Value { get; }

    public Box(T value)
    {
        Value = value;
    }
}

var box = new Box<string>("hello");
string text = box.Value;

Benefits:

  • Compile-time type safety
  • Less casting
  • Better readability
  • Better reuse
  • Better performance for value types because generic collections avoid many boxing scenarios
  • Clearer APIs because the expected type is part of the method or class signature

Common interview point: generics are not just about avoiding duplicate code. They also express intent and let the compiler catch errors earlier.

What Type Constraints Are

A type constraint tells the compiler what a type parameter must support.

Code
public static T Max<T>(T left, T right)
    where T : IComparable<T>
{
    return left.CompareTo(right) >= 0 ? left : right;
}

Without where T : IComparable<T>, the compiler cannot allow left.CompareTo(right) because it cannot assume every possible T has a CompareTo method.

A constraint acts like a contract between the reusable component and the caller:

  • The component promises to work for any type that satisfies the constraint.
  • The caller must provide a type that satisfies the constraint.
  • The compiler validates the rule before runtime.

Basic Generic Syntax

Generic classes:

Code
public class Repository<TEntity>
{
    private readonly List<TEntity> _items = new();

    public void Add(TEntity entity)
    {
        _items.Add(entity);
    }

    public IReadOnlyList<TEntity> GetAll()
    {
        return _items;
    }
}

Generic methods:

Code
public static T FirstOrDefaultValue<T>(IEnumerable<T> items, T fallback)
{
    foreach (var item in items)
    {
        return item;
    }

    return fallback;
}

Generic interfaces:

Code
public interface IHandler<TRequest, TResponse>
{
    Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);
}

Generic delegates:

Code
public delegate TResult Converter<in TInput, out TResult>(TInput input);

In real applications, generic interfaces are common in CQRS, validation, mapping, pipelines, caching, and event handling.

The where Keyword

Constraints are declared using where clauses.

Code
public class EfRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
    where TKey : notnull
{
    public Task<TEntity?> FindAsync(TKey id)
    {
        throw new NotImplementedException();
    }
}

This means:

  • TEntity must be a non-nullable reference type.
  • TEntity must implement IEntity<TKey>.
  • TKey must be non-nullable.

Multiple type parameters can have separate constraints.

Code
public interface IMapper<TSource, TDestination>
    where TSource : class
    where TDestination : class, new()
{
    TDestination Map(TSource source);
}

Common Constraint Types

C# supports several important constraint forms.

where T : class

Requires T to be a reference type.

Code
public class ReferenceCache<T>
    where T : class
{
    private readonly Dictionary<string, T> _cache = new();

    public T? Get(string key)
    {
        return _cache.TryGetValue(key, out var value) ? value : null;
    }
}

In a nullable-enabled project, where T : class means T is a non-nullable reference type. Use class? when nullable reference type arguments are allowed.

where T : class?

Allows nullable or non-nullable reference types.

Code
public static bool IsMissing<T>(T value)
    where T : class?
{
    return value is null;
}

This is useful when the component intentionally accepts nullable reference values.

where T : struct

Requires T to be a non-nullable value type.

Code
public static bool IsDefault<T>(T value)
    where T : struct
{
    return EqualityComparer<T>.Default.Equals(value, default);
}

Important details:

  • struct excludes nullable value types such as int?.
  • struct implies an accessible parameterless constructor.
  • struct cannot be combined with class, class?, notnull, unmanaged, or new().

where T : notnull

Requires T to be a non-nullable type.

Code
public class Lookup<TKey, TValue>
    where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _items = new();

    public void Add(TKey key, TValue value)
    {
        _items.Add(key, value);
    }
}

This is common for dictionary keys, cache keys, IDs, and strongly typed lookup structures.

Important interview detail: notnull works with nullable reference type analysis. Violations normally produce compiler warnings rather than hard errors.

where T : unmanaged

Requires T to be an unmanaged value type.

Code
public static int SizeOf<T>()
    where T : unmanaged
{
    return System.Runtime.InteropServices.Marshal.SizeOf<T>();
}

This is useful for low-level code, interop, binary serialization, memory operations, and performance-sensitive algorithms.

Important details:

  • unmanaged implies struct.
  • unmanaged cannot be combined with struct or new().
  • It is not commonly needed in normal business applications.

where T : new()

Requires a public parameterless constructor.

Code
public static T Create<T>()
    where T : new()
{
    return new T();
}

This is useful for simple factories, mapping components, test helpers, and object initialization.

Important details:

  • new() must appear last when combined with other constraints.
  • It only allows public parameterless construction.
  • It does not work for constructors with parameters.
  • Overusing new() can lead to weak object creation because dependencies and invariants may be bypassed.

Base Class Constraint

Requires T to inherit from a specific base class.

Code
public abstract class Entity
{
    public Guid Id { get; init; }
}

public class AuditService<TEntity>
    where TEntity : Entity
{
    public string GetAuditKey(TEntity entity)
    {
        return $"{typeof(TEntity).Name}:{entity.Id}";
    }
}

Base class constraints are useful when a reusable component needs shared behavior or state from a base class.

Trade-off: base class constraints couple the generic component to a specific inheritance hierarchy. Interface constraints are often more flexible.

Interface Constraint

Requires T to implement an interface.

Code
public interface IEntity<TKey>
{
    TKey Id { get; }
}

public class Repository<TEntity, TKey>
    where TEntity : IEntity<TKey>
    where TKey : notnull
{
    public TKey GetId(TEntity entity)
    {
        return entity.Id;
    }
}

Interface constraints are one of the most common and useful constraint types because they express behavior without forcing inheritance from a base class.

Common examples:

Code
where T : IDisposable
where T : IComparable<T>
where T : IEquatable<T>
where T : IEntity<Guid>
where T : ICommand<TResult>
where T : IValidator<TRequest>

Multiple Constraints

A type parameter can have multiple constraints.

Code
public class CsvExporter<T>
    where T : class, IExportable, new()
{
    public string ExportNewItem()
    {
        var item = new T();
        return item.ToCsv();
    }
}

public interface IExportable
{
    string ToCsv();
}

Constraint ordering matters:

Code
// Correct
where T : class, IExportable, new()

// Incorrect
// where T : new(), class, IExportable

General order:

  1. Primary kind constraint such as class, class?, struct, notnull, or unmanaged
  2. Base class constraint, if used
  3. Interface constraints
  4. new() constraint last
  5. Anti-constraints such as allows ref struct, when applicable

Enum Constraints

C# supports constraining a type parameter to System.Enum.

Code
public static IReadOnlyList<TEnum> GetEnumValues<TEnum>()
    where TEnum : struct, Enum
{
    return Enum.GetValues<TEnum>();
}

This is useful for reusable enum helpers.

Code
public enum OrderStatus
{
    Draft,
    Submitted,
    Paid,
    Cancelled
}

var values = GetEnumValues<OrderStatus>();

Why this is better than accepting Enum directly:

  • Stronger type safety
  • No need to pass typeof(OrderStatus) manually
  • Easier to build reusable helpers
  • Better caller experience

Delegate Constraints

A type can be constrained to Delegate or MulticastDelegate.

Code
public static TDelegate Combine<TDelegate>(TDelegate first, TDelegate second)
    where TDelegate : Delegate
{
    return (TDelegate)Delegate.Combine(first, second);
}

This is useful for reusable delegate utilities, callback composition, and event-related infrastructure.

In everyday business code, delegate constraints are less common than interface and base class constraints, but they are useful to know for advanced interviews.

Type Parameter as a Constraint

One type parameter can constrain another.

Code
public static void CopyTo<TSource, TDestination>(
    IEnumerable<TSource> source,
    ICollection<TDestination> destination)
    where TSource : TDestination
{
    foreach (var item in source)
    {
        destination.Add(item);
    }
}

This means every TSource must be assignable to TDestination.

Code
List<string> strings = ["a", "b"];
List<object> objects = [];

CopyTo<string, object>(strings, objects);

This is useful when modeling relationships between type parameters.

Generic Methods vs Generic Classes

A generic class makes the whole type reusable for a type parameter.

Code
public class PagedResult<T>
{
    public IReadOnlyList<T> Items { get; init; } = [];
    public int TotalCount { get; init; }
}

A generic method makes only one operation generic.

Code
public static PagedResult<T> ToPagedResult<T>(
    IReadOnlyList<T> items,
    int totalCount)
{
    return new PagedResult<T>
    {
        Items = items,
        TotalCount = totalCount
    };
}

Use a generic class when the type parameter is part of the object's state or identity.

Use a generic method when the type parameter is only needed for one operation.

Common mistake: making an entire class generic when only one method needs the type parameter.

Constraints and Nullable Reference Types

Generic constraints interact with nullable reference types.

Code
public class RequiredValue<T>
    where T : notnull
{
    public T Value { get; }

    public RequiredValue(T value)
    {
        Value = value;
    }
}

This prevents nullable values from being used as valid type arguments in nullable-aware code.

Compare these constraints:

Code
where T : class      // non-nullable reference type in nullable context
where T : class?     // nullable or non-nullable reference type
where T : notnull    // non-nullable reference type or non-nullable value type
where T : struct     // non-nullable value type

Important habit: choose the constraint that matches the real domain rule.

Code
public class Cache<TKey, TValue>
    where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _cache = new();

    public TValue? GetOrDefault(TKey key)
    {
        return _cache.TryGetValue(key, out var value) ? value : default;
    }
}

TKey should be notnull because dictionary keys should not be null. TValue may or may not be nullable depending on the business case.

Generic Constraints and Reusable Domain Components

Generics are common in domain and application layers.

Code
public interface IEntity<TKey>
{
    TKey Id { get; }
}
Code
public interface IRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
    where TKey : notnull
{
    Task<TEntity?> FindByIdAsync(TKey id, CancellationToken cancellationToken);
    Task AddAsync(TEntity entity, CancellationToken cancellationToken);
}
Code
public sealed class Product : IEntity<Guid>
{
    public Guid Id { get; init; }
    public string Name { get; set; } = string.Empty;
}

This design makes the repository reusable for many entity types while still guaranteeing that each entity has an ID.

Interview trade-off:

  • Good: type-safe, reusable, consistent abstraction
  • Bad: can become too generic and hide important persistence details
  • Bad: generic repositories may not fit complex aggregate-specific queries
  • Best practice: use generic abstractions where behavior is truly common, and use specific repositories or query services for domain-specific operations

Generic Constraints in CQRS and MediatR-Style Designs

CQRS patterns often use generics for commands, queries, handlers, and pipeline behaviors.

Code
public interface ICommand<TResult>
{
}

public interface ICommandHandler<TCommand, TResult>
    where TCommand : ICommand<TResult>
{
    Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
}
Code
public sealed record CreateProductCommand(string Name) : ICommand<Guid>;
Code
public sealed class CreateProductHandler
    : ICommandHandler<CreateProductCommand, Guid>
{
    public Task<Guid> HandleAsync(
        CreateProductCommand command,
        CancellationToken cancellationToken)
    {
        var productId = Guid.NewGuid();
        return Task.FromResult(productId);
    }
}

The constraint ensures that a handler can only be created for a request type that actually represents a command returning the expected result.

Generic Constraints for Validation

Reusable validation pipelines often use generic constraints.

Code
public interface IValidator<T>
{
    IReadOnlyList<string> Validate(T instance);
}

public sealed class ValidationBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public IReadOnlyList<string> Validate(TRequest request)
    {
        return _validators
            .SelectMany(validator => validator.Validate(request))
            .ToList();
    }
}

This style is common in production .NET applications because it allows cross-cutting behavior to be implemented once and reused for many request types.

Generic Constraints for Factories

The new() constraint can be used for simple factories.

Code
public interface IHasCreatedAt
{
    DateTime CreatedAtUtc { get; set; }
}

public static class Factory
{
    public static T Create<T>()
        where T : IHasCreatedAt, new()
    {
        return new T
        {
            CreatedAtUtc = DateTime.UtcNow
        };
    }
}

However, in real applications, constructors often require dependencies or required values.

Better for domain objects:

Code
public sealed class Order
{
    public Guid Id { get; }
    public string CustomerNumber { get; }

    public Order(Guid id, string customerNumber)
    {
        Id = id;
        CustomerNumber = string.IsNullOrWhiteSpace(customerNumber)
            ? throw new ArgumentException("Customer number is required.", nameof(customerNumber))
            : customerNumber;
    }
}

Avoid using new() if it forces domain objects to have weak parameterless constructors.

Generic Math and Static Abstract Interface Members

Modern C# supports static abstract members in interfaces, which enables generic math and reusable numeric algorithms.

Code
public static T Add<T>(T left, T right)
    where T : System.Numerics.IAdditionOperators<T, T, T>
{
    return left + right;
}

Before static abstract interface members, writing reusable numeric code was difficult because operators like + could not be expressed through normal interface constraints.

Code
public static T Sum<T>(IEnumerable<T> values)
    where T :
        System.Numerics.IAdditionOperators<T, T, T>,
        System.Numerics.IAdditiveIdentity<T, T>
{
    var total = T.AdditiveIdentity;

    foreach (var value in values)
    {
        total += value;
    }

    return total;
}

This is useful for numeric algorithms, financial calculations, scientific code, statistics helpers, and reusable math libraries.

Interview point: this is an advanced feature. Most business applications do not need it, but it shows how constraints can describe compile-time capabilities beyond normal instance methods.

Variance in Generic Interfaces

Variance controls how generic interface types can be assigned when their type arguments have inheritance relationships.

Covariance uses out and is for producer/output positions.

Code
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

This works because IEnumerable<out T> only produces values of T.

Contravariance uses in and is for consumer/input positions.

Code
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer;

This works because an IComparer<object> can compare strings, since strings are objects.

Custom examples:

Code
public interface IProducer<out T>
{
    T Produce();
}

public interface IConsumer<in T>
{
    void Consume(T item);
}

Rules:

  • Use out T when the interface returns T but does not accept T as input.
  • Use in T when the interface accepts T as input but does not return T.
  • Value types do not support variance conversions.
  • Classes are invariant even if they implement variant interfaces.

Common interview trap:

Code
List<string> strings = new();
IEnumerable<object> objects = strings; // Valid

// List<object> objectList = strings; // Invalid

List<T> is invariant because it both consumes and produces T.

Open Generic Types and Dependency Injection

In ASP.NET Core and other .NET applications, open generic registrations allow one implementation to be reused for many closed generic types.

Code
services.AddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));

This means the container can resolve:

Code
IRepository<Product, Guid>
IRepository<Customer, int>
IRepository<Order, Guid>

A common validation pipeline example:

Code
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

This is powerful because reusable generic components can be wired once and applied across many application types.

Interview point: open generics are often used for repositories, validators, handlers, decorators, pipeline behaviors, mappers, and caching wrappers.

Runtime Behavior and Performance

Generics are checked at compile time, but they also have runtime behavior.

Important practical points:

  • Generic code avoids many casts that would be needed with object.
  • Generic collections such as List<int> avoid boxing each int.
  • Reflection-based generic creation is more flexible but slower and less type-safe.
  • Generic constraints let the compiler call constrained members directly.
  • Overly abstract generic designs can make debugging and stack traces harder to understand.

Example of avoiding boxing:

Code
var numbers = new List<int>();
numbers.Add(10); // No boxing into object

Compared with:

Code
var numbers = new ArrayList();
numbers.Add(10); // int is boxed as object

In modern C#, prefer generic collections and generic interfaces over non-generic collections.

Practical Design Examples

Reusable Result Type

Code
public sealed class Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(bool isSuccess, T? value, string? error)
    {
        IsSuccess = isSuccess;
        Value = value;
        Error = error;
    }

    public static Result<T> Success(T value)
    {
        return new Result<T>(true, value, null);
    }

    public static Result<T> Failure(string error)
    {
        return new Result<T>(false, default, error);
    }
}

This generic type is reusable for many response values.

Code
Result<Guid> result = Result<Guid>.Success(Guid.NewGuid());
Result<string> failed = Result<string>.Failure("Name is required.");

Strongly Typed ID Pattern

Code
public interface IStronglyTypedId<TValue>
    where TValue : notnull
{
    TValue Value { get; }
}

public readonly record struct ProductId(Guid Value)
    : IStronglyTypedId<Guid>;

public readonly record struct CustomerId(Guid Value)
    : IStronglyTypedId<Guid>;

This avoids mixing unrelated IDs accidentally.

Code
public sealed class Product
{
    public ProductId Id { get; init; }
}

Generic constraints can then work with typed IDs:

Code
public static string FormatId<TId, TValue>(TId id)
    where TId : IStronglyTypedId<TValue>
    where TValue : notnull
{
    return id.Value.ToString()!;
}

Generic Cache Wrapper

Code
public interface ICache<TKey, TValue>
    where TKey : notnull
{
    bool TryGet(TKey key, out TValue value);
    void Set(TKey key, TValue value);
}

public sealed class MemoryCache<TKey, TValue> : ICache<TKey, TValue>
    where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _items = new();

    public bool TryGet(TKey key, out TValue value)
    {
        return _items.TryGetValue(key, out value!);
    }

    public void Set(TKey key, TValue value)
    {
        _items[key] = value;
    }
}

The notnull constraint clearly expresses that cache keys cannot be null.

Generic Comparable Helper

Code
public static class SortHelper
{
    public static IReadOnlyList<T> Sort<T>(IEnumerable<T> items)
        where T : IComparable<T>
    {
        return items.OrderBy(item => item).ToList();
    }
}

The constraint allows sorting only for types that define comparison logic.

Constraints vs Runtime Checks

A constraint is checked at compile time.

Code
public static void DisposeItem<T>(T item)
    where T : IDisposable
{
    item.Dispose();
}

A runtime check is checked while the program runs.

Code
public static void DisposeIfPossible<T>(T item)
{
    if (item is IDisposable disposable)
    {
        disposable.Dispose();
    }
}

Use constraints when the operation requires a capability.

Use runtime checks when the capability is optional.

Interview comparison:

ApproachUse WhenBenefitTrade-off
ConstraintThe type must support the behaviorCompile-time safetyLess flexible
Runtime checkThe behavior is optionalMore flexibleErrors may be found later
ReflectionShape is unknown at compile timeMaximum flexibilitySlower and less safe
Interface polymorphismYou do not need generic type preservationSimple abstractionMay lose specific type information

Constraints vs Inheritance vs Interfaces

Generic constraints can use both base classes and interfaces, but the design intent is different.

Base class constraint:

Code
where T : Entity

Use when all types truly share a common base implementation.

Interface constraint:

Code
where T : IEntity<Guid>

Use when the component only needs a behavior or contract.

Generic interface without base class:

Code
public interface IAuditable
{
    DateTime CreatedAtUtc { get; }
}
Code
public static DateTime GetCreatedAt<T>(T item)
    where T : IAuditable
{
    return item.CreatedAtUtc;
}

In most reusable business components, interface constraints are more flexible than base class constraints.

Common Mistakes

Overusing Generics

Not every abstraction needs generics.

Code
public class ProductService<TProduct>
{
    // Only ever used with Product
}

If only one type is valid, use the concrete type.

Over-Constraining Type Parameters

Code
public class ReportBuilder<T>
    where T : class, new()
{
}

If the class never creates T, the new() constraint is unnecessary.

Only add constraints that the implementation actually needs.

Using new() for Domain Entities

Code
public class EntityFactory<T>
    where T : new()
{
    public T Create() => new T();
}

This can force domain entities to expose parameterless constructors and allow invalid objects.

Prefer explicit factories for domain objects with required data.

Forgetting notnull for Keys

Code
public class Cache<TKey, TValue>
{
    private readonly Dictionary<TKey, TValue> _items = new();
}

Better:

Code
public class Cache<TKey, TValue>
    where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _items = new();
}

Keys should usually be non-null.

Assuming default(T) Is Always Safe

Code
public static T GetDefault<T>()
{
    return default!;
}

default(T) can be:

  • 0 for int
  • false for bool
  • null for reference types
  • A zero-initialized struct
  • A value that may be invalid for the domain

Avoid using default as a fake valid value unless the behavior is clearly documented.

Confusing class, class?, and notnull

Code
where T : class

This means non-nullable reference type in nullable-aware code.

Code
where T : class?

This allows nullable reference types.

Code
where T : notnull

This allows non-nullable reference types and non-nullable value types.

Choose based on the real nullability rule.

Assuming IEnumerable<T> Allows Modification

Code
public void AddItem<T>(IEnumerable<T> items, T item)
{
    // Cannot add to IEnumerable<T>
}

IEnumerable<T> is for enumeration. Use ICollection<T> or IList<T> if mutation is required.

Returning Overly Generic Types

Code
public object GetValue<T>()
{
    return default(T)!;
}

Better:

Code
public T? GetValue<T>()
{
    return default;
}

If the method is generic, preserve the generic type information.

Best Practices

Use generics when the logic is genuinely reusable across multiple types.

Prefer interface constraints for behavior-based reuse:

Code
where T : IAuditable

Use base class constraints only when shared implementation is truly required:

Code
where T : Entity

Use notnull for dictionary keys, cache keys, IDs, and lookup types.

Avoid adding constraints before the implementation actually needs them.

Use nullable-aware constraints such as class, class?, and notnull intentionally.

Prefer generic collections such as List<T>, Dictionary<TKey, TValue>, and IReadOnlyList<T> over non-generic collections.

Avoid new() constraints for rich domain models that require valid constructor arguments.

Keep generic APIs readable. If a generic signature becomes hard to understand, the abstraction may be too broad.

Use descriptive type parameter names when the role is not obvious:

Code
public interface IRepository<TEntity, TKey>

This is clearer than:

Code
public interface IRepository<T, U>

Use common names such as T, TKey, TValue, TEntity, TRequest, TResponse, TCommand, and TQuery.

Prefer specific abstractions for domain-specific behavior. A generic repository should not become a dumping ground for every possible query.

Interview Mental Model

When choosing a generic constraint, ask:

  1. What operation does the generic component need to perform?
  2. Does every possible T support that operation?
  3. Can an interface express the required behavior?
  4. Is a base class truly required?
  5. Should the type allow reference types, value types, nullable values, or only non-null values?
  6. Does the component need to create instances of T?
  7. Would a non-generic abstraction be simpler?
  8. Will this generic design remain understandable to other developers?

A good generic design is reusable but not vague. It should make invalid usage impossible or at least difficult.

Interview Practice

PreviousDelegatesNext UpIEnumerable vs IQueryable in C#