I'm trying to do basic eager loading on a list of ProjectVersions where each ProjectVersion has a list of FieldValues and ChildProjects. I want the FieldValues and ChildProjects to be loaded along with all their properties when ProjectVersions is loaded, but it seems that in my code when going through each ProjectVersion, it still hits the database to get these collections (checking sql server profiler). Any pointers would be helpful.
var publishedList = Repository.Find<Project>().//a bunch of wheres and selects
IEnumerable<ProjectVersion> publishedList = published
.Include(x => x.FieldValues)
.Include(x => x.ChildProjects)
.ToList();
//EDIT: the context is hidden behind a generic Repository. Below are some details:
public class Repository : IRepository
{
internal readonly IDataContext _context;
public Repository(IDataContext context)
{
_context = context;
_context.Committed += _context_Committed;
_context.Deleted += _context_Deleted;
}
public IQueryable<T> Find<T>() where T : class, IEntity
{
return _context.Repository<T>();
}
}
public class EfDataContext : IDataContext
{
public IQueryable<T> Repository<T>() where T : class, IEntity
{
var table = _context.Set(typeof(T));
WrappedFieldsObjectQuery<T>(table.Cast<T>().AsExpandable()));
return table.Cast<T>().AsExpandable();
}
}
public class MsmDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type =>
type.IsClass &&
type.BaseType.IsGenericType &&
type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var config in typesToRegister.Select(Activator.CreateInstance))
{
modelBuilder.Configurations.Add((dynamic)config);
}
base.OnModelCreating(modelBuilder);
}
}
public class ProjectMapping : EntityTypeConfiguration<Project>
{
public ProjectMapping()
{
HasOptional(p => p.LastChangedBy).WithMany(p => p.ProjectsChanged).WillCascadeOnDelete();
HasRequired(p => p.CreatedBy).WithMany(p => p.ProjectsCreated).WillCascadeOnDelete();
HasRequired(d => d.Account).WithMany(p => p.Projects).WillCascadeOnDelete();
HasRequired(d => d.PinType).WithMany(p => p.Projects).HasForeignKey(p => p.PinType_Id).WillCascadeOnDelete();
}
}
public static class RepositoryFactory
{
public static IRepository CreateRepository()
{
return CreateEfRepository();
}
internal static IRepository CreateEfRepository()
{
return new Repository(new EfDataContext(new MsmDbContext()));
}
}
Ok, I can't see your complete query, but in your comments you wrote something about select. EF will ignore Include() statements as soon as you are doing custom projections using Select(), my guess is that's why eager loading does not work. Instead of Include(), try to add the properties you want to load to your projection, something like
Repository.Find<Project>()
.Select(p => new { project = p, p.FieldValues, p.ChildProjects })
.AsEnumerable().Select(p => p.project).ToList()
This way the projection will take care of loading your data, you don't need Include().
Actually, I got it to work by just directly working with the DataContext. Somehow the Repository is screwing it up.
Related
Does anybody know how to remove a global filter on run time?
I have a multi-tenant application that includes a global filter as tenant id.
I want to have a super-tenant that does not have this filter therefore when I detected a super tenant I want to remove this filter from the configuration of entities.
-There is an IgnoreQueryFilters but it is only useful locally.
Global Filter Reference Microsoft
https://learn.microsoft.com/en-us/ef/core/querying/filters
public class MerchantConfiguration : TenantEntityConfiguration<Merchant>
{
public MerchantConfiguration(ITenantContext context) : base(context) { }
public override void Configure(EntityTypeBuilder<Merchant> builder)
{
base.Configure(builder);
builder.ToTable($"AppMerchants");
builder.HasKey(e => new { e.MerchantId, e.TenantId });
builder.Property(e => e.PostalCode).IsRequired();
builder.Property(e => e.IndustryCode).IsRequired();
builder.Property(e => e.State).HasMaxLength(3);
builder.HasOne(e => e.MerchantCategory).WithMany().HasForeignKey(e => e.MerchantCategoryCode).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Industry).WithMany().HasForeignKey(e => e.IndustryCode).OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(e => e.LocationId);
builder.HasIndex(e => e.PostalCode);
builder.HasIndex(e => e.City);
builder.HasIndex(e => e.Name);
}
}
public class TenantEntityConfiguration<TEntity> : AuditableEntityConfiguration<TEntity>
where TEntity : class, ITenantEntity
{
protected readonly ITenantContext context;
public TenantEntityConfiguration(ITenantContext context)
{
this.context = context;
}
public override void Configure(EntityTypeBuilder<TEntity> builder)
{
base.Configure(builder);
builder.Property(e => e.TenantId).HasMaxLength(64);
builder.HasIndex(e => e.TenantId);
builder.HasQueryFilter(e => e.TenantId == context.TenantId);
}
}
Well, will try to show simplest sample, when defining filters in context's OnModelCreating method
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => this.CurrentTenantId == null
|| this.IsSuperTenant
|| b.TenentId == this.CurrentTenantId.Value);
It expects that you DbCcontext has this property:
public class MyDbContext : DbContext
{
public int? CurrentTenantId { get; set; }
...
}
I am using EF Core 3.1.7. The DbContext has the UseLazyLoadingProxies set. Fluent API mappings are being used to map entities to the database. I have an entity with a navigation property that uses a backing field. Loads and saves to the database seem to work fine except for an issue when accessing the backing field before I access the navigation property.
It seems that referenced entities don't lazy load when accessing the backing field. Is this a deficiency of the Castle.Proxy class or an incorrect configuration?
Compare the Student class implementation of IsRegisteredForACourse to the IsRegisteredForACourse2 for the behavior in question.
Database tables and relationships.
Student Entity
using System.Collections.Generic;
namespace EFCoreMappingTests
{
public class Student
{
public int Id { get; }
public string Name { get; }
private readonly List<Course> _courses;
public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();
protected Student()
{
_courses = new List<Course>();
}
public Student(string name) : this()
{
Name = name;
}
public bool IsRegisteredForACourse()
{
return _courses.Count > 0;
}
public bool IsRegisteredForACourse2()
{
//Note the use of the property compare to the previous method using the backing field.
return Courses.Count > 0;
}
public void AddCourse(Course course)
{
_courses.Add(course);
}
}
}
Course Entity
namespace EFCoreMappingTests
{
public class Course
{
public int Id { get; }
public string Name { get; }
public virtual Student Student { get; }
protected Course()
{
}
public Course(string name) : this()
{
Name = name;
}
}
}
DbContext
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreMappingTests
{
public sealed class Context : DbContext
{
private readonly string _connectionString;
private readonly bool _useConsoleLogger;
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public Context(string connectionString, bool useConsoleLogger)
{
_connectionString = connectionString;
_useConsoleLogger = useConsoleLogger;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter((category, level) =>
category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information)
.AddConsole();
});
optionsBuilder
.UseSqlServer(_connectionString)
.UseLazyLoadingProxies();
if (_useConsoleLogger)
{
optionsBuilder
.UseLoggerFactory(loggerFactory)
.EnableSensitiveDataLogging();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(x =>
{
x.ToTable("Student").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasMany(p => p.Courses)
.WithOne(p => p.Student)
.OnDelete(DeleteBehavior.Cascade)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Course>(x =>
{
x.ToTable("Course").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasOne(p => p.Student).WithMany(p => p.Courses);
});
}
}
}
Test program which demos the issue.
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;
namespace EFCoreMappingTests
{
class Program
{
static void Main(string[] args)
{
string connectionString = GetConnectionString();
using var context = new Context(connectionString, true);
var student2 = context.Students.FirstOrDefault(q => q.Id == 5);
Console.WriteLine(student2.IsRegisteredForACourse());
Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities
Console.WriteLine(student2.IsRegisteredForACourse());
}
private static string GetConnectionString()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
return configuration["ConnectionString"];
}
}
}
Console Output
False
True
True
When you declare a mapped property in an EF entity as virtual, EF generates a proxy which is capable of intercepting requests and assessing whether the data needs to be loaded. If you attempt to use a backing field before that virtual property is accessed, EF has no "signal" to lazy load the property.
As a general rule with entities you should always use the properties and avoid using/accessing backing fields. Auto-initialization can help:
public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();
these are my beginnings with EfCore (earlier I was in nHibernate and Dapper).
I have a problem with mappings.
My model looks like that:
public class Document
{
public Guid Id {get;set;}
public string Name {get;set;}
public int ValueIDontWantToBeInDb {get; set;}
}
My mappings:
b.ToTable("documents");
b.Property(x => x.Id).ValueGeneratedOnAdd();
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
(where b is EntityTypeBuilder received in IEntityTypeConfiguration implementation.
As you can see, I never use ValueIDontWantToBeInDb, but EfCore keeps adding this to table schema. Why is it so and what to do to make it add only those properties that I want?
I know there is a Ignore method. But then I would have to call it on every model on every property that I do not want to be added to schema.
I just want to show to EfCore - "Hey, map these properties like so" - just like in nHibernate. How to do this?
Ok, I create some solution for this. But I think that it's really outreagous that EfCore doesn't have such a solution in standard. So, first create base class for all your mappings. This is really the place with all the magic:
abstract class BaseMap<T>: IEntityTypeConfiguration<T> where T: class, IDbItem //IDbItem is my own constraint, just an interface that has Id property
{
EntityTypeBuilder<T> theBuilder;
List<string> mappedPropertyNames = new List<string>();
protected PropertyBuilder<TProperty> Map<TProperty>(Expression<Func<T, TProperty>> x) //this will be called instead of Property()
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.Property(x);
}
protected ReferenceNavigationBuilder<T, TRelatedEntity> HasOne<TRelatedEntity>(Expression<Func<T, TRelatedEntity>> x)
where TRelatedEntity: class
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.HasOne(x);
}
protected CollectionNavigationBuilder<T, TRelatedEntity> HasMany<TRelatedEntity>(Expression<Func<T, IEnumerable<TRelatedEntity>>> x)
where TRelatedEntity: class
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.HasMany(x);
}
protected PropertyBuilder<TColumnType> Map<TColumnType>(string propName)
{
mappedPropertyNames.Add(propName);
return theBuilder.Property<TColumnType>(propName);
}
protected abstract void CreateModel(EntityTypeBuilder<T> builder);
public void Configure(EntityTypeBuilder<T> builder)
{
theBuilder = builder;
Map(x => x.Id).ValueGeneratedOnAdd();
builder.HasKey(x => x.Id);
CreateModel(builder);
IgnoreUnmappedProperties(builder);
}
void IgnoreUnmappedProperties(EntityTypeBuilder<T> builder)
{
PropertyInfo[] propsInModel = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var prop in propsInModel)
{
if (!mappedPropertyNames.Contains(prop.Name))
{
builder.Ignore(prop.Name);
}
}
}
string GetPropertyName<TProperty>(Expression<Func<T, TProperty>> memberExpression)
{
MemberExpression member = null;
switch (memberExpression.Body)
{
case UnaryExpression ue when ue.Operand is MemberExpression:
member = ue.Operand as MemberExpression;
break;
case MemberExpression me:
member = me;
break;
default:
throw new InvalidOperationException("You should pass property to the method, for example: x => x.MyProperty");
}
var pInfo = member.Member as PropertyInfo;
if (pInfo == null)
throw new InvalidOperationException("You should pass property to the method, for example: x => x.MyProperty");
return pInfo.Name;
}
}
Now example of a class that derives from this and creates real object map:
class DocumentMap : BaseMap<Document>
{
protected override void CreateModel(EntityTypeBuilder<Document> b)
{
b.ToTable("documents");
Map(x => x.Name).IsRequired();
Map<Guid>("Owner_id").IsRequired();
HasOne(x => x.Owner)
.WithMany()
.HasForeignKey("Owner_id")
.HasConstraintName("FK_Users_Documents")
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
HasMany(x => x.Periods)
.WithOne(y => y.ParentDocument);
HasMany(x => x.Loans)
.WithOne(y => y.ParentDocument);
}
}
Notice that instead of using methods from EntityTypeBuilder, I use methods derived from base class. Frankly speaking I even don't have to pass EntityTypeBuilder object here.
And all is called in DbContext like this:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
My application does something strange, if I execute the following code outside the debugger it does not work, but if I run it with the debugger it works fine: keeping in mind that I step through the code, and not continue the next instant.
As I can gather from the debugger, there is nothing wrong with the code, but maybe there is, I at least cannot find it.
public void Reject(string id)
{
using (context)
{
int intId = Convert.ToInt32(id);
Hours hr = (from h in context.Hours
where h.Id == intId
select h).FirstOrDefault();
hr.Status = 30;
context.SaveChanges();
}
}
ApplicationDBContext class
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
static ApplicationDbContext()
{
System.Data.Entity.Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());
}
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public DbSet<Department> Departments { get; set; }
public DbSet<Tasks> Tasks { get; set; }
public DbSet<Hours> Hours { get; set; }
public DbSet<OffDays> OffDays { get; set; }
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>()
.HasRequired(u => u.Department)
.WithMany(d => d.Users)
.Map(c => c.MapKey("DepartmentId"));
modelBuilder.Entity<ApplicationUser>()
.HasMany(u => u.Supervisors)
.WithMany();
modelBuilder.Entity<Department>()
.HasMany(d => d.Supervisors)
.WithOptional()
.Map(c => c.MapKey("SupervisorId"));
modelBuilder.Entity<Hours>()
.HasRequired(u => u.UserId)
.WithMany(h => h.Hours)
.Map((c => c.MapKey("Hours")));
}
}
Do you guys have an idea what I can try?
It looks like you are reusing the same DbContext instance through the life of your application. This goes against Microsoft's recommended best practices, and may introduce some of the odd behavior you are seeing. DbContext manages a LOT of cached data inside each DbContext instance, and keeping it alive for the entire application can cause all sort of craziness as different cached entities are returned. This caching is what I think is causing your problem. Somewhere in your application this instance is getting disconnected from the context, and now that disconnected, cached instance is being returned the next time you ask for it.
The current Entity Framework best practices recommend that you create a new instance of the context for each logical "unit of work".
The easiest way to create a new context instance on demand is like this:
using (var context = new ApplicationDbContext())
{
//Code here that uses the context
}
You must first attach the entity that you have retrieved
public void Reject(string id)
{
using (context)
{
int intId = Convert.ToInt32(id);
Hours hr = (from h in context.Hours
where h.Id == intId
select h).FirstOrDefault();
context.Attach( hr);// here is difference
hr.Status = 30;
context.SaveChanges();
}
}
I've been struggling with Breeze to SaveChanges to a projection and admit I new to both EF and breeze. There were some similar questions earlier when I was trying to use WCF, but now I have abandoned WCF and added EF directly to my solution.
In my controller I return the DTO for the metadata to breeze along with the DTO and it binds perfectly.
After altering the data on the client my Breese Controllers [HttpPost] SaveChanges(save Bundle) is called and map contains the DTO and the changes.
How Do I persist the Changes? If I re-read the DTO projection for breeze to update then EF cant save a projection because it's not "tracked", if I read the Full entity, then Breeze error with "Sequence contains no matching element" because its looking for the DTO? Am I suppose to use AutoMapper?
Controller:
[BreezeController]
public class BreezeController : ApiController
{
readonly EFContextProvider<ManiDbContext> _contextProvider = new EFContextProvider<ManiDbContext>();
[HttpGet]
public string Metadata()
{
return _contextProvider.Metadata();
}
[HttpGet]
public IQueryable<ConsigneDTO> Consignee(string refname)
{
return _contextProvider.Context.consigneDTO(refname);
}
[HttpPost]
public SaveResult SaveChanges(JObject saveBundle)
{
ManiDbContextProvider _mcontextProvider = new ManiDbContextProvider();
return _mcontextProvider.SaveChanges(saveBundle);
}
ManiDbContext (the main DBContext is CifContext which is Database First/EF Reverse Engineer)
public class ManiDbContext : DbContext
{
public CifContext CifDbContext = new CifContext();
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
Database.SetInitializer<ManiDbContext>(null);
modelBuilder.Configurations.Add(new ConsigneDTOMap());
}
public override int SaveChanges()
{
CifDbContext.SaveChanges();
return 1;
}
public IQueryable<ConsigneDTO> consigneDTO(string refname)
{
IQueryable<ConsigneDTO> q = this.CifDbContext.Consignes
.Where(x => x.Refname == refname)
.Select(f => new ConsigneDTO {Refname = f.Refname, Consignee = f.Consignee, Address1 = f.Address1, Address2 = f.Address2, Address3 = f.Address3});
return q;
}
ManiDbContextProvider
public class ManiDbContextProvider : EFContextProvider<CifContext>
// public class ManiDbContextProvider : EFContextProvider<ManiDbContext>
{
public ManiDbContextProvider() : base() { }
protected override void OpenDbConnection()
{// do nothing
}
protected override void CloseDbConnection()
{ // do nothing
}
protected override bool BeforeSaveEntity(EntityInfo entityInfo)
{
var entity = entityInfo.Entity;
if (entity is ConsigneDTO)
{
return BeforeSaveConsignee(entity as ConsigneDTO, entityInfo);
}
throw new InvalidOperationException("Cannot save entity of unknown type");
}
private bool BeforeSaveConsignee(ConsigneDTO c, EntityInfo info)
{
var consdata = this.Context.CifDbContext.Consignes
.Where(x => x.Refname == c.Refname)
.FirstOrDefault(); // ENTITY
// var consdata = this.Context.consigneDTO(c.Refname); // DTO
return (null != consdata) || throwCannotFindConsignee();
}
CifContext (Full Columns - First/EF Reverse Engineer/ Consigne class contains Keys)
public partial class CifContext : DbContext
{
static CifContext()
{
Database.SetInitializer<CifContext>(null);
}
public CifContext()
: base("Name=CifContext")
{
}
public DbSet<Consigne> Consignes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); // Use singular table names
Database.SetInitializer<CifContext>(null);
modelBuilder.Configurations.Add(new ConsigneMap());
}
Regardless if I Read the Entity or the DTO - I'm Clueless on how breeze updates EF
Any Help greatly appreciated :)
Regards,
Mike