0

ASP .NET 5 MVC Core application.

Following method is used to find entity by key:

    class MyStudents: Students { }

    public TEntity FindNormalized<TEntity>(params object[] keyValues)
        where TEntity : class
    { 
        return Find<TEntity>(keyValues);
    }

    void FindNormalized<TEntity>(TEntity entity, params object[] keyValues)
        where TEntity : class
    {
        var res = FindNormalized<TEntity>(keyValues);
        if (res == null)
            throw new KeyNotFoundException(entity.GetType().Name + " entity key not found " + keyValues[0]);
        CopyPropertiesTo(res, entity);
    }

    static void CopyPropertiesTo<T, TU>(T source, TU dest)
    { // https://stackoverflow.com/questions/3445784/copy-the-property-values-to-another-object-with-c-sharp
        var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList();
        var destProps = typeof(TU).GetProperties()
                .Where(x => x.CanWrite)
                .ToList();

        foreach (var sourceProp in sourceProps)
            if (destProps.Any(x => x.Name == sourceProp.Name))
            {
                var p = destProps.First(x => x.Name == sourceProp.Name);
                p.SetValue(dest, sourceProp.GetValue(source, null), null);
            }
    }

Using it with subclass

   FindNormalized<MyStudents>(1);

throws exception

Cannot create a DbSet for 'MyStudents' because this type is not included in the model for the context.

   FindNormalized<Students>(1);

works.

How to fix this so that it can used with subclass type also ?

For getting table attribute from subclass code from Dynamic Linq works:

    string GetTableAttrib(Type t)
    {
        foreach (Type t2 in SelfAndBaseClasses(t))
        {
            IEntityType entityType = Model.FindEntityType(t2);
            if (entityType == null)
                continue;
            return entityType.GetTableName();
        }
        throw new ApplicationException(t.FullName + " no table attribute");
    }

    /// <summary>
    /// Enumerate inheritance chain - copied from DynamicLinq
    /// </summary>
    static IEnumerable<Type> SelfAndBaseClasses(Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }

Maybe this can used to implement Find also. Unfortunately Find throws exception. Is it reasonable to wrap Find using try/catch or is there better way ?

Andrus
  • 26,339
  • 60
  • 204
  • 378
  • it's fairly obvious that what you can obtain from `EFCore` is just the entity instances (that does make sense, `EFCore` does not manage non-entity types). But you want the derived entity instances instead. So that's the problem of converting from the entity instance to a derived entity instance. That can be solved by the so-called `object cloning` or `object copying`. You normalize the problem first and solve the finally analyzed problem which is simple. – King King Jan 23 '21 at 22:56
  • BTW, your kind of design with types deriving from entity types is fairly weird. There should be some better design than that. I've never implemented something like that, I do use interfaces however. – King King Jan 23 '21 at 22:59
  • There is one Poco document object in DBContext. It has shild objects: Invoice, Order, Waybill, Offer. Each of them have different attributes, eq. document name, acces right, subtype in database, Icon. Invoice object needs to get data from database as passes its type to Find. – Andrus Jan 23 '21 at 23:13
  • what returned type do you expect from this `FindNormalized(1)`? `Students` or `MyStudents`? If it's `MyStudents` then you must perform some kind of cloning/copying (as I said before), otherwise you may have it solvable with reflection. – King King Jan 23 '21 at 23:18
  • It should return `Students` object. Returned property values are copied to MyStudents property values. – Andrus Jan 23 '21 at 23:34
  • then the generic method `TEntity FindNormalized` cannot be used. Because the returned type `Students` is different from the type T of `MyStudents`. If you want to derive the returned type from the type argument T, the returned type is then like a runtime type (cannot be clearly involved with any design time type). That means usually at best we can just define the return type as `object` or some base entity type. – King King Jan 23 '21 at 23:49
  • `CopyPropertiesTo` method is used to copy properties as descibed in `https://stackoverflow.com/questions/3445784/copy-the-property-values-to-another-object-with-c-sharp` I updated question and added code to copy properties. – Andrus Jan 23 '21 at 23:58
  • your new updated question makes more sense, I've come up with a solution, check out my answer below and try it out, if there is any error, let me know in the comment because I've not tested it at all. – King King Jan 24 '21 at 23:07

1 Answers1

1

The EFCore Find method throws exception because the TEntity is not an entity type but a type deriving from the entity type. So to solve this, we need to first get the most derived type which is the ancestor type of the passed in TEntity type, so when passing in MyStudents you need to get type Students first to use as type argument for Find<> method. Because that type is not known at design time, so we need to use reflection to invoke that Find<> method or better use the other overload of Find that accepts the first argument of entity type (Type). Of course I understand that your Find<> method in your question is from DbContext.Find.

So the solution can be simple like this, firstly we need method to get the most derived entity type from the passed in type (which inherits from the entity type):

public static class DbContextExtensions {
     public static Type GetMostDerivedEntityType(this DbContext dbContext, Type type){
         while(type != null && dbContext.Model.FindEntityType(type) == null) {
             type = type.BaseType;
         }
         return type;
     }
}

Next just use that extension method in your FindNormalized method to find the most derived entity type first before using Find:

//I suppose that this is in the context (class) of your custom DbContext
void FindNormalized<TEntity>(TEntity entity, params object[] keyValues)
    where TEntity : class
{
    var actualEntityType = this.GetMostDerivedEntityType(typeof(TEntity)) ?? 
                           throw new InvalidOperationException($"The type {typeof(TEntity)} is not an entity type or a derive from an entity type");
    //use the actualEntityType instead
    var res = Find(actualEntityType, keyValues);
    if (res == null)
        throw new KeyNotFoundException(entity.GetType().Name + " entity key not found " + keyValues[0]);
    //copy the properties
    CopyPropertiesTo(res, entity);
}

Note that in the above code I use another overload (non-generic version) of Find which accepts the first argument of Type (entity type) directly instead of using your wrapper FindNormalized which is generic only (you can add your one overload of that wrapper which wraps the non-generic Find as used directly in my code above).

I've not tested the code at all. Just try it and let me know if there is any error.

King King
  • 61,710
  • 16
  • 105
  • 130