Search code examples
c#.netreflection

Filtering class properties by a specific criteria


Consider the following model (.NET 8, C# 12):

public partial interface IEntity
{
    public long Id { get; set; }
}

public partial interface IEntity<TEntity>
    where TEntity : class, IEntity, IEntity<TEntity>, new()
{
}

public partial class User: IEntity, IEntity<User>
{
    public long Id { get; set; }
}

public partial class Currency: IEntity, IEntity<Currency>
{
    public long Id { get; set; }
}

public partial class ApplicationDbContext: DbContext
{
    public virtual DbSet<User> Users { get; set; }
    public virtual DbSet<Currency> Currencies { get; set; }

    //// TODO: Fix to get a list of DbSets.
    //public static List<object> GetDbSets () => typeof(ApplicationDbContext)
    //  .GetProperties(BindingFlags.Instance | BindingFlags.Public)
    //  .Where(p => p.CanRead && p.CanWrite)
    //  .Where(p => p.GetGetMethod(nonPublic: false) != null)
    //  .Where(p => p.GetSetMethod(nonPublic: false) != null)
    //  .Where(p => p.PropertyType == typeof(DbSet<>))
    //  .Cast<object>()
    //  .ToList();

    //// TODO: Fix to get a list of entities.
    //public static List<Type> GetEntities () => typeof(ApplicationDbContext)
    //  .GetProperties(BindingFlags.Instance | BindingFlags.Public)
    //  .Where(p => p.CanRead && p.CanWrite)
    //  .Where(p => p.GetGetMethod(nonPublic: false) != null)
    //  .Where(p => p.GetSetMethod(nonPublic: false) != null)
    //  .Where(p => p.PropertyType.IsGenericType)
    //  .Where(p => p.PropertyType.Name.StartsWith(typeof(DbSet<>).Name))
    //  //.Where(p => p.PropertyType == typeof(DbSet<IEntity>))
    //  .Select(p => p.PropertyType.GetGenericArguments() [0])
    //  //.Cast<object>()
    //  .ToList();

    public static List<Type> GetEntityTypesOrderedDelete () => new []
    {
        typeof(User),
        typeof(Account),
        typeof(Transaction),
        typeof(AccountCategory),
        typeof(Currency),
    }
    .ToList();
}

private static void Main ()
{
}

I want to get a list of properties List<System.Reflection.PropertyInfo> values using the following criteria:

  • Properties must be instance, public, and virtual.
  • Properties must inherit from DbSet<IEntity<TEntity>>.
  • The generic Dbset properties must have exactly one generic parameter IEntity<TEntity>.
  • The generic parameter IEntity<TEntity> must have exactly one nested generic parameter TEntity.
  • TEntity must adhere to the generic type constraints enforced by IEntity<TEntity>.

The first two methods of the ApplicationDbContext class do not return anything. As a workaround, I took a hard-coded approach in the third method, which of course is not sustainable for production code.

The ideal solution would have the following signature:

public static ReadOnlyCollection<PropertyInfo> GetApplicationDbContextDbSets ()
{
    var properties = typeof(ApplicationDbContext)
        .GetProperties();

    return (properties.AsReadOnly());
}

I must be missing something or using the APIs incorrectly.

Any advice would be appreciated.


Solution

  • This filtering should fulfill you requirements:

    public static ReadOnlyCollection<PropertyInfo> GetApplicationDbContextDbSets()
    {
        var properties = typeof(ApplicationDbContext)
            // Properties must be instance, public, and virtual.
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(x => x.GetGetMethod()?.IsVirtual == true)
            // Properties must inherit from DbSet<IEntity<TEntity>>
            .Where(x => x.PropertyType.IsGenericType)
            .Where(x => x.PropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(DbSet<>)))
            .Where(x => x.PropertyType.GetGenericArguments().Length == 1)
            // The generic Dbset properties must have exactly one generic parameter IEntity<TEntity>
            .Where(x => x.PropertyType.GetGenericArguments()[0]
                .GetInterfaces()
                .Where(y => y.IsGenericType)
                .Where(y => y.GetGenericTypeDefinition().IsAssignableFrom(typeof(IEntity<>)))
                // The generic parameter IEntity<TEntity> must have exactly one nested generic parameter TEntity.
                .Where(y => y.GetGenericArguments().Length == 1)
                // TEntity must adhere to the generic type constraints enforced by IEntity<TEntity>
                .Where(y => y.GetGenericArguments()[0].IsAssignableFrom(x.PropertyType.GetGenericArguments()[0])).Any()
            )
            .ToList();
    
        return (properties.AsReadOnly());
    }
    

    In production code I would write this as full for each loop instead of LINQ to have better debuggability and less duplicated expressions.