primary hasforeignkey framework foreign example c# entity-framework find

c# - hasforeignkey - entity framework core primary key



Filtro de Entity Framework por PrimaryKey (3)

Estoy escribiendo un servicio crud genérico. Estoy tratando de implementar el método Get con un método virtual opcional para incluir propiedades. Sin embargo, estoy teniendo problemas porque FindAsync solo se declara en un DbSet :

public async virtual Task<TDTO> Get(object[] id) { // I want to do something like this var entity = await this.ApplyGetIncludes(this.GetEntityDBSet()).FindAsync(id) return this.AdaptToDTO(entity); } protected virtual DbSet<TEntity> GetEntityDBSet() { return this._context.Set<TEntity>(); } protected virtual IQueryable<TEntity> ApplyGetIncludes(IQueryable<TEntity> queryable) { return queryable; }

Quiero hacer algo como esto como se muestra arriba:

var entity = await this.ApplyGetIncludes(this.GetEntityDBSet()).FindAsync(id)

pero sé que eso no funcionará porque necesitamos el conjunto de bases de datos así que me gustaría hacer algo como esto:

var entity = await this.ApplyGetIncludes(this.GetEntityDBSet().FilterByPK(id)) .FirstOrDefaultAsync();

¿Alguien sabe cómo puedo filtrar por clave principal desde un DbSet ?


Es posible, pero el método necesita acceso a DbContext para obtener los metadatos que describen la clave principal. Luego puede construir dinámicamente la expresión lambda basada en esos metadatos y los valores pasados.

Primero, necesitamos un método que reúna información sobre las propiedades de la clave primaria de la entidad.

Para EF Core es simple:

static IReadOnlyList<IProperty> GetPrimaryKeyProperties(DbContext dbContext, Type clrEntityType) { return dbContext.Model.FindEntityType(clrEntityType).FindPrimaryKey().Properties; }

Para EF6 es un poco más complicado, pero todavía factible:

struct KeyPropertyInfo { public string Name; public Type ClrType; } public static IReadOnlyList<KeyPropertyInfo> GetPrimaryKeyProperties(DbContext dbContext, Type clrEntityType) { var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; var metadata = objectContext.MetadataWorkspace; var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace)); var entityType = metadata.GetItems<EntityType>(DataSpace.OSpace) .Single(e => objectItemCollection.GetClrType(e) == clrEntityType); return entityType.KeyProperties .Select(p => new KeyPropertyInfo { Name = p.Name, ClrType = p.PrimitiveType.ClrEquivalentType }) .ToList(); }

Ahora el método para construir el predicado es así:

static Expression<Func<T, bool>> BuildKeyPredicate<T>(DbContext dbContext, object[] id) { var keyProperties = GetPrimaryKeyProperties(dbContext, typeof(T)); var parameter = Expression.Parameter(typeof(T), "e"); var body = keyProperties // e => e.PK[i] == id[i] .Select((p, i) => Expression.Equal( Expression.Property(parameter, p.Name), Expression.Convert( Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"), p.ClrType))) .Aggregate(Expression.AndAlso); return Expression.Lambda<Func<T, bool>>(body, parameter); }

La parte difícil aquí es cómo dejar que EF use consultas parametrizadas. Si simplemente usamos Expression.Constant(id[i]) , el SQL generado usará valores constantes en lugar de parámetros. Entonces, el truco es usar la expresión de acceso de miembro (es decir, propiedad o campo) de una expresión constante de tipo anónimo temporal que tenga el valor (básicamente simulando el cierre).

Una vez que obtenga el predicado del método anterior, puede usarlo para FirstOrDefaultAsync o cualquier otro método de filtrado.


Tu pregunta parece un poco difícil. En mi opinión, es imposible lograr su objetivo mediante un método genérico para filtrar por clave principal a todas las tablas. El Id en tu código anterior, significa las teclas de la Tabla (DBSet). Y debe tratar el ID de manera diferente de acuerdo con la consulta de Tabla diferente. De esta manera, creo que sería mejor utilizar un método abstracto como el siguiente para obtener los datos

public async abstract Task<TDTO> Get(object[] id) { //I want to do something like this var entity = await this.ApplyGetIncludes(this.GetEntityDBSet()).FindAsync(id) return this.AdaptToDTO(entity); }

Debe implementar su método Get de acuerdo con sus tablas concretas (mientras que cada tabla generalmente tiene claves principales diferentes).


Me tomé la libertad de hacer algunos métodos de extensión para hacerlo más fácil. En la actualidad, debes pasar en el contexto, porque es difícil obtener el contexto de su campo privado.

public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext) { return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties; } public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id) { var keyProperties = dbContext.GetPrimaryKeyProperties<T>(); var parameter = Expression.Parameter(typeof(T), "e"); var body = keyProperties // e => e.PK[i] == id[i] .Select((p, i) => Expression.Equal( Expression.Property(parameter, p.Name), Expression.Convert( Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"), p.ClrType))) .Aggregate(Expression.AndAlso); return Expression.Lambda<Func<T, bool>>(body, parameter); } public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id) where TEntity : class { return dbSet.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id)); }