0

I'm trying to implement common logic for database entity classes, so I'd like to introduce a common, not mapped, abstract base class for my entities like this.

public abstract class BaseEntity {
    public int Id { get; set; }
    public byte[] RowVersion { get; set; }
}

I don't want to represent this base class in the database, but only at a logical level in the entity model. The project is a Database-First project, having an EDMX model. If it was Code-First, I could (I guess) easily achieve this by annotating the base class properties with the proper EF annotations (Key, etc.).

But in the EDMX it seems like I'm unable to select a custom base type which is not an entity itself. When I tried to add a new entity, it complained about the fact that it's not mapped.

I've also tried using an interface like IDbEntity having the above 2 properties, but when I write a generic method like the following (just quick example), I get error about the interface property not being mapped. Seems like EF cannot recognize that basically the Id access corresponds to the unerlying entity type.

public static TDbEntity[] GetByIds<TDbEntity>(this IQueryable<TDbEntity> queryable, int[] ids)
    where TDbEntity : IDbEntity
{
    return queryable.Where(e => ids.Contain(e.Id)).ToArray();
}

I simply cannot find a viable option for this kind of base class when using Database-First. Any help would be appreciated.

Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • Is there any specific reason you're making your base class abstract? you could just leave it to be a normal class with annotations, and inherit from it... – Glenn van Acker Dec 05 '19 at 08:49
  • Implicitly implemented interface combined with `class` constraint in generic methods (e.g. `where TDbEntity : class, IDbEntity`) usually works. – Ivan Stoev Dec 05 '19 at 08:54
  • @GlennvanAcker AFAIK when using Database-First, annotations will not work properly because all the mapping information comes from EDMX, but I might be wrong. – Zoltán Tamási Dec 05 '19 at 08:56
  • @IvanStoev thanks, I missed the `class` constraint, I'll give it a try. – Zoltán Tamási Dec 05 '19 at 08:56
  • If you using Database-First then why you need Base Entity? As i supposed your every table has ```Id``` which self care by EDXM when generate from Database – Dharmeshsharma Dec 05 '19 at 09:00
  • @ZoltánTamási You may be right actually, but a quick google search brought this up, maybe it helps: https://stackoverflow.com/questions/22263189/entity-framework-db-first-implement-inheritance – Glenn van Acker Dec 05 '19 at 09:02
  • as im using in my current project which is not database first nor code first my base classs is ```public abstract partial class BaseEntity { /// /// Gets or sets the entity identifier /// public int ID { get; set; } }``` and applied on my Entity ```public partial class CategoryMaster : BaseEntity``` then im able to get this – Dharmeshsharma Dec 05 '19 at 09:04
  • @Dharmeshsharma I need the base class to be able to write the common logic in a generic way to avoid repeating myself (quering, concurrency, deletion, etc.). If I just write the class manually then it works, however, when the EDMX is saved first, it will regenerate all the classes, so the base class will disappear. – Zoltán Tamási Dec 05 '19 at 09:26
  • @IvanStoev I'm kind of surprised, but the `class` constraint together with the interface technique did the trick, thank you. I'm quite curious about the why. I think I could modify the corresponding T4 to add the interface to my entities. If you could post it as a solution I could accept it. – Zoltán Tamási Dec 05 '19 at 09:28
  • @ZoltánTamási Same happened with me every time, i created one interface and tried to apply on my ```Entities``` but every time update EDMX my Interface disappear and i have to manually apply every time so i moved to manually mapping like https://github.com/nopSolutions/nopCommerce but in your and mine case i didn't find solution due to EDMX auto update and every time apply the interface – Dharmeshsharma Dec 05 '19 at 09:34
  • @Dharmeshsharma You can attach the interface in another `partial class` file, though I don't like it too much. I'm planning to edit the T4 template to generate the interface where it's appropriate (entities having an `int Id` property), so it will get auto-generated on EDMX save. – Zoltán Tamási Dec 05 '19 at 09:48

1 Answers1

0

For future readers, I'm posting my current solution.

Based on @IvanStoev's comment, the only thing I needed using the interface technique is the class generic constraint. So this version of my extension method works fine on entity queryies implemeing the IDbEntity interface.

public interface IDbEntity {
    int Id { get; }
}

...

public static TDbEntity[] GetByIds<TDbEntity>(this IQueryable<TDbEntity> queryable, int[] ids)
    where TDbEntity : class, IDbEntity
{
    return queryable.Where(e => ids.Contain(e.Id)).ToArray();
}

Then I modified my Edmx model T4 generator file to generate the interface references where it's needed. I had to extend the following method in the following way.

public string EntityClassOpening(EntityType entity)
{
    var baseTypeNames = new List<string>();
    baseTypeNames.Add(_typeMapper.GetTypeName(entity.BaseType));

    var hasId = entity.Properties.Any(e => e.Name == @"Id" && _typeMapper.UnderlyingClrType(e.TypeUsage.EdmType) == typeof(int));

    if (hasId) {
        baseTypeNames.Add(@"IDbEntity");
    }

    return string.Format(
            CultureInfo.InvariantCulture,
            "{0} {1}partial class {2}{3}",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            _code.StringBefore(" : ", string.Join(", ", baseTypeNames.Where(e => !string.IsNullOrEmpty(e)))));
}
Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93