With the below 1 to many relationship setup:
public partial class Folder
{
public Folder()
{
Files = new HashSet<File>();
}
public int FolderId { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public partial class File
{
public int FileId { get; set; }
public int FolderId { get; set; }
public virtual Folder Folder { get; set; }
}
public class DataContext: DbContext
{
...
public virtual DbSet<Folder> Folders { get; set; }
public virtual DbSet<File> Files { get; set; }
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Folder>(entity =>
{
...
});
modelBuilder.Entity<File>(entity =>
{
...
entity.HasOne(e => e.Folder)
.WithMany(e => e.Files)
.HasForeignKey(e => e.FolderId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("...");
});
}
}
public SomeReturnType AFunction(IQueryable<Folder> folders) =>
folders.GroupJoin(
...,
(folder, other) => new
{
folder,
something = folder.Files.Any(...)
});
With AFunction, the related Files are loaded for folder.Files.Any(). But if I do the below:
public static class FolderExtensions
{
public static SomeReturnType BFunction(this Folder folder) =>
folder.Files.Any(...);
}
public SomeReturnType AFunction(IQueryable<Folder> folders) =>
folders.GroupJoin(
...,
(folder, other) => new
{
folder,
something = folder.BFunction()
});
BFunction will not load the related Files. Any ideas to get around this problem?
To explain what happens let's first reduce the query to the essentials:
folders.Select(folder => new
{
folder,
something = folder.BFunction()
});
The problem here is that the BFunction method is executed client-side. That is, the part IQueryable<Folder> folders is first materialized into memory (client-side) and then the projection (new { ... }) is applied to the materialized result. This is something EF core does if anything in the final projection (and only in the final projection) can't be translated into SQL. Since Files are not Include-ed and no lazy loading occurs, the materialized folders don't have files and BFunction evaluates to false.
EF doesn't try to expand CLR functions into expression trees itself. In this case it would be possible, but often it won't be. At any rate, EF doesn't even try.
You have to do the expansion yourself:
folders.Select(folder => new
{
folder,
something = folder.Files.Any()
});
Including the files would also work, but of course that's more expensive:
folders
.Include(f => f.Files)
.Select(folder => new
{
folder,
something = folder.BFunction()
});
Side-note: the GroupJoin in this form is presently not supported by EF core (6.0).
Related
I am working on a simple web API that is just supposed to parse a JSON tree, and save it to a database. I am working with EF Core 6.0.4 and my application shows a really weird behaviour: right after saving the tree, it loads from the context just fine. But when I call a different endpoint and load the data using a freshly initialized context, the children won't load. I believe it's due to the EF config, but I can't figure out how to load the children.
I tried using _context.Entity(returnValue).Collection(x => x.Children) but the x in the lambda only has ICollection extension methods, not the model fields like in the code examples I saw.
I also tried using .Include, but that seems to be a thing from regular EF.
Here's some of my code:
Controller
public class CategoryTreeManagerController : ControllerBase
{
private readonly CategoryTreeManagerService _service;
public CategoryTreeManagerController(CategoryTreeManagerService service)
{
_service = service;
}
[HttpGet]
public IEnumerable<CategoryTreeNode> GetTree()
{
return _service.GetTree(); //this only returns the root node without any children
}
[HttpPost]
public IEnumerable<CategoryTreeNode> SaveTree(CategoryTreeNode[] nodes)
{
_service.SaveTree(nodes[0]);
return _service.GetTree(); //this correctly returns the tree
}
}
Service
public class CategoryTreeManagerService
{
private readonly CategoryTreeManagerApiDbContext _context;
public CategoryTreeManagerService(CategoryTreeManagerApiDbContext context)
{
_context = context;
}
public IEnumerable<CategoryTreeNode> GetTree()
{
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
return returnValue;
}
public void SaveTree(CategoryTreeNode node)
{
if (_context.CategoryTreeNodes.Any(x => x.Id == node.Id))
{
_context.Update(node);
}
else
{
_context.CategoryTreeNodes.Add(node);
}
_context.SaveChanges();
}
}
DbContext
public class CategoryTreeManagerApiDbContext : DbContext
{
public CategoryTreeManagerApiDbContext() : base ()
{
Database.EnsureCreated();
}
public CategoryTreeManagerApiDbContext(DbContextOptions<CategoryTreeManagerApiDbContext> options) : base(options)
{
Database.EnsureCreated();
}
public DbSet<CategoryTreeNode> CategoryTreeNodes { get; set; }
public DbSet<TreeNodeDetail> TreeNodeDetails { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CategoryTreeNode>(entity =>
{
entity.HasKey(x => x.Id);
entity.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
});
}
Model classes
public class CategoryTreeNode
{
public int Id { get; set; }
public string Label { get; set; }
public string Story { get; set; }
public string Icon { get; set; }
public ICollection<TreeNodeDetail> Details { get; set; }
public ICollection<CategoryTreeNode> Children { get; set; }
[JsonIgnore]
public CategoryTreeNode? Parent { get; set; }
}
public class TreeNodeDetail
{
[JsonPropertyName("detailId")]
public string Id { get; set; }
[JsonPropertyName("detailTitle")]
public string Title { get; set; }
[JsonPropertyName("detailValue")]
public string Value { get; set; }
[JsonIgnore]
[ForeignKey("CategoryTreeNode")]
public int CategoryTreeNodeId { get; set; }
}
Include would work for one level, or more if you expand out the expression, but it's not an ideal solution for tree structures which could be variable depth. (I.e. child of a child of a child ...)
For instance:
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Include(x => x.Children)
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
would load all parents and their first level children. To load 2nd level children:
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Include(x => x.Children)
.ThenInclude(x => x.Children)
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
The issue is knowing how many levels to load, and each level produces a Cartesian Product for EF to work through, exponentially increasing the amount of data being loaded to build a tree. Loading an entire table once quickly becomes a much more efficient solution.
If you have a Single tree structure where you expect only one top-level entry, or want to load an entire reasonable set of top-level nodes then loading all entries into memory will work since EF will be tracking all of the entities and it can resolve all of the various references as it builds the entity structure. This has to load the entire set even if you only want one specific parent out of several possible parents.
If you have several top level parent nodes and a sizeable overall table size to work through, and do want to be able to load a single parent and it's children then one option is to add a de-normalized top-level ID reference to the tree node.
public int Id { get; set; }
public int? TopLevelId { get; set; }
This would be a null-able FK but does not need a navigation property. The current Parent reference would continue to use a shadow property. (I.e. ParentId) In this way once you have an ID for the top-level parent you want to load a tree for:
_context.CategoryTreeNodes.Where(x => x.TopLevelId == topLevelId).ToList();
var topLevelNode = _context.CategoryTreeNodes.Single(x => x.Id == topLevelId);
The first statement will have the DBContext load and track all nodes under that top level tree node. Then when you call the DbContext to get that top level node, the tracked related entities will all get filled in.
The caveat of this approach is that it is a denormalization, in that there is no DB-level assertion that a TopLevelId is set, or remains set correctly. For instance if nodes can be moved between top-level entities and you forget to update this value, this node would not be loaded and associated under the new parent using the above load.
I am trying to separate my contexts using DDD. I have two domains, Instruments and Advertisements with its aggregate roots (the example is hypothetical). Instrument AR owns many InstrumentPictures and I would like to have that information in the Advertisement domain as well via proxy entity.
To ensure good database integrity it would be better to create foreign key from AdvertisementPicture.Guid to InstrumentPicture.Guid but as far as I know this can be done only through HasOne/HasMany model configuration.
Am I using the owner relationship wrong?
(Note: I do not want to configure the FK with custom sql migration.)
Instrument AR:
public class Instrument
{
protected Instrument()
{
}
public Instrument(string name, IEnumerable<InstrumentPicture> pictures)
{
Name = name;
_instrumentPictures.AddRange(pictures);
}
protected List<InstrumentPicture> _instrumentPictures = new List<InstrumentPicture>();
public IReadOnlyCollection<InstrumentPicture> InstrumentPictures
=> _instrumentPictures.AsReadOnly();
public Guid Guid { get; private set; }
public string Name { get; private set; }
}
InstrumentPicture owned collection:
public class InstrumentPicture
{
protected InstrumentPicture()
{
}
public InstrumentPicture(Guid guid, string url)
{
Guid = guid;
Url = url;
}
public Guid Guid { get; set; }
public string Url { get; set; }
public DateTime Created { get; set; }
}
Advertisiment AR
public class Advertisement
{
protected Advertisement()
{
}
public Advertisement(Guid instrumentGuid, string name, IEnumerable<AdvertisementPicture> pictures)
{
InstrumentGuid = instrumentGuid;
Name = name;
_advertisementPictures.AddRange(pictures);
}
protected List<AdvertisementPicture> _advertisementPictures = new List<AdvertisementPicture>();
public IReadOnlyCollection<AdvertisementPicture> AdvertisementPictures
=> _advertisementPictures.AsReadOnly();
public Guid Guid { get; private set; }
public Guid InstrumentGuid { get; private set; }
public string Name { get; private set; }
}
AdvertisementPicture proxy
public class AdvertisementPicture
{
protected AdvertisementPicture()
{
}
public AdvertisementPicture(Guid guid, string url)
{
Guid = guid;
Url = url;
}
public Guid Guid { get; set; }
public string Url { get; set; }
}
Model configuration:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Instrument>()
.HasKey(e => e.Guid);
modelBuilder.Entity<Instrument>()
.OwnsMany(e => e.InstrumentPictures, pic =>
{
pic.HasKey(e => e.Guid);
});
modelBuilder.Entity<Advertisement>()
.HasKey(e => e.Guid);
modelBuilder.Entity<Advertisement>()
.HasOne<Instrument>()
.WithMany()
.HasForeignKey(e => e.InstrumentGuid);
modelBuilder.Entity<Advertisement>()
.OwnsMany(e => e.AdvertisementPictures, pic =>
{
pic.HasKey(e => e.Guid);
// How can I add a foreign key to original InstrumentPicture for database integrity?
});
}
I've been struggling with this for hours and finding lots of answers on SO saying this isn't possible. Turns out this is possible using EntityFrameworkCore so I'll post what I've found on my Top Google Search for this problem.
As soon as you add a foreign key you will find the migration tool attempting to create the table in the second DBContext (unless you add ModelBuilder.Ignore<>() which will either do nothing or ignore your foreign key depending on your order of operations).
You can however do something like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}
This will allow you to reference tables in other DBContext's but exclude any changes to them from the one you're working in. This is outlined in the MS documentation here.
If you have used Fluent API you may still need to apply those configurations in the referencing DB Context. This is easily achieved if you have used the IEntityTypeConfiguration<T> implementation by an additional call to ModelBuilder.ApplyConfigurationsFromAssembly(typeof(T).Assembly);.
In such a use case as above you may find yourself excluding a lot of different entities from your DB context. If you have these defined in their own library like I have to follow a DDD pattern you may find an extension method useful to exclude all of them at once:
public static class ExcludeEntitiesInAssemblyFromMigrationsExtension
{
public static void ExcludeEntitiesInAssemblyFromMigrations(this ModelBuilder builder, Assembly assembly)
{
var assemblyTypes = assembly.GetExportedTypes().Where(t => t.IsClass && !t.IsAbstract);
foreach (var assemblyType in assemblyTypes)
{
var entityBuilder = builder.Entity(assemblyType);
var entityTablename = entityBuilder.Metadata.GetTableName();
if (entityTablename != null)
{
entityBuilder.ToTable(entityTablename, t => t.ExcludeFromMigrations());
}
}
}
}
For the life of me, I cannot get this to work properly. I have a relatively simple domain model that has a couple of navigation properties that I want to fill out via eager loading.
To keep my domain model pure, I have opted to use shadow properties as foreign keys, so they are not accessible by the client code.
This is the domain model:
public class CourseType : Entity
{
protected CourseType() { }
public CourseType(string name, CoachGroup coachGroup, bool active)
{
Name = name;
CoachGroup = coachGroup;
Active = active;
}
public string Name { get; private set; }
private int? _coachGroupId;
private int? CoachGroupId => _coachGroupId;
public virtual CoachGroup CoachGroup { get; private set; }
public int? AgeLimit { get; private set; }
public bool Active { get; private set; }
public bool ShowInSearchForm { get; private set; }
private List<Course> _accessGivingCourses = new List<Course>();
public virtual IReadOnlyList<Course> AccessGivingCourses => _accessGivingCourses?.ToList();
}
This is how I wire the configuration up:
public class CourseTypeConfiguration : IEntityTypeConfiguration<CourseType>
{
public void Configure(EntityTypeBuilder<CourseType> builder)
{
builder.ToTable("CourseTypes", "Courses");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).HasColumnName("CourseTypeId");
builder.Property(p => p.Name).HasColumnName("CourseTypeName");
builder.Property(p => p.Active).HasColumnName("IsActive");
builder.Property(p => p.ShowInSearchForm).HasColumnName("ShowInSearchForm");
builder.Property(p => p.AgeLimit).HasColumnName("AgeLimit");
builder.Property<int?>("CoachGroupId").HasField("_coachGroupId");
builder.HasOne(p => p.CoachGroup).WithOne().HasForeignKey<CourseType>("CoachGroupId").OnDelete(DeleteBehavior.Restrict);
builder.HasMany(p => p.AccessGivingCourses).WithMany("AccessGivingCourses")
.UsingEntity<Dictionary<string, object>>("CourseTypesAccessGivingCourses",
j => j.HasOne<Course>().WithMany().HasForeignKey("CourseId"),
j => j.HasOne<CourseType>().WithMany().HasForeignKey("CourseTypeId"),
j => j.ToTable("CourseTypesAccessGivingCourses")
);
builder.HasIndex("CoachGroupId").IsUnique(false);
}
}
This is how I extract the data via my repository class:
public override async Task<IEnumerable<CourseType>> GetAll()
{
try
{
return await Context.CourseTypes.Include(i => i.CoachGroup).Include(i => i.AccessGivingCourses).ToListAsync();
}
catch (Exception e)
{
Logger.LogCritical(e, $"Could not retrieve list of course type entities");
throw;
}
}
It ALMOST works, except for the fact that when I add or update entities, the CoachGroup link randomly gets lost for some updates. For others, it works just fine. It's like Entity Framework Core OR the database randomly loses track of it. Which is odd, because when I look in the database table, the foreign keys in the table are all there like they're supposed to!?
Does anyone have any idea what the hell I am doing wrong? Or if this is the correct approach to this problem at all? All I want to do is to load related data, but it's getting to the point where it's becoming rocket science to merely link a couple of optional relationships together...
I try to use EF core, but only a part of my model is saved to the database.
This is my model:
public class EngineType
{
public string Name { get; set; }
}
public class Car
{
public long CarId { get; set; }
public string Name { get; set; }
public EngineType Engine { get; set; }
}
The CarId and the Name is saved, but not the EngineType.
This is the test I use, but actual.Engine is always null:
[TestMethod]
public void WhenIAddAndSaveANewCarThenItIsAddedToDB()
{
using var target = new EFCoreExampleContext();
using var concurrentContext = new EFCoreExampleContext();
var expected = new Car() {CarId = 0815, Name = "Isetta", Engine = new EngineType() { Name = "2Takt" }};
target.Cars.Add(expected);
target.SaveChanges();
var actual = concurrentContext.Cars.Single();
Assert.AreEqual(1, concurrentContext.Cars.Count());
Assert.IsNotNull(actual.Engine);
Assert.AreEqual(expected, actual);
}
My Context looks like this:
public class EFCoreExampleContext : DbContext
{
public DbSet<Car> Cars { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase(databaseName: "Add_writes_to_database");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EngineType>(
d =>
{
d.HasKey(e => e.Name);
d.Property(e => e.Name).IsRequired();
});
modelBuilder.Entity<EngineType>(
d =>
{
d.HasKey(e => e.Name);
});
modelBuilder.Entity<Car>(
d =>
{
d.HasKey(e => e.CarId);
d.Property<DateTime>("LastChanged").IsRowVersion().ValueGeneratedOnAddOrUpdate();
d.Property<string>("EngineForeignKey");
d.HasOne(e => e.Engine)
.WithMany()
.HasForeignKey("EngineForeignKey")
.IsRequired();
});
}
}
Any idea what am I doing wrong (or which existing topic answers this question - I even didn't have the right search words to find it).
Thanks!
I think there is no issue with saving. Entity Framework does not do eager loading by default. So you have to explicitly include any navigational properties that should be in result. Try this when you are fetching actual,
using Microsoft.EntityFrameworkCore;
var actual = concurrentContext.Cars.Include(c => c.Engine).Single();
When serializing a MongoDB document to a POCO is there any way to make properties map case insensitive? For example I'd like this document:
{
"id": "1"
"foo": "bar"
}
to map to this class:
public MyObj
{
public int Id {get; set;}
public string Foo {get; set;}
}
To do that I think you will have 2 options.
The first would be to write out a class map manually
BsonClassMap.RegisterClassMap<MyClass>(cm => {
cm.AutoMap();
cm.GetMemberMap(c => c.Foo).SetElementName("foo");
});
The second would be to decorate your class with the following attributes
public class MyObj
{
[BsonElement("id")]
public int Id { get; set; }
[BsonElement("foo")]
public string Foo { get; set; }
}
The CSharp driver team have a good tutorial on serialization on the following link
http://docs.mongodb.org/ecosystem/tutorial/serialize-documents-with-the-csharp-driver/
Update
I have just tried the following and this works for me, obviously I'm sure this is a much more simplified version of your code but taking a guess at how it might look.
I have registered the two class maps separately and added the BsonKnownType to the base class.
[BsonKnownTypes(typeof(GeoJSONObject))]
public class Point
{
public string Coordinates { get; set; }
}
public class GeoJSONObject : Point
{
public string Type { get; set; }
}
static void Main(string[] args)
{
var cn = new MongoConnectionStringBuilder("server=localhost;database=MyTestDB;");
var settings = MongoClientSettings.FromConnectionStringBuilder(cn);
var client = new MongoClient(settings);
BsonClassMap.RegisterClassMap<Point>(cm =>
{
cm.AutoMap();
cm.GetMemberMap(c => c.Coordinates).SetElementName("coordinates");
});
BsonClassMap.RegisterClassMap<GeoJSONObject>(cm =>
{
cm.AutoMap();
cm.GetMemberMap(c => c.Type).SetElementName("type");
});
var result = client.GetServer()
.GetDatabase("MyTestDB")
.GetCollection("MyCol")
.Find(Query.EQ("type", BsonValue.Create("xxxx")));
}
I see that it is old question, but people still may search it. At least I found it while was asking the same question.
The CamelCaseElementNameConvention can be used to apply this globally.
var pack = new ConventionPack();
pack.Add(new CamelCaseElementNameConvention());
ConventionRegistry.Register("Camel case convention", pack, t => true);
Documentation: https://mongodb.github.io/mongo-csharp-driver/2.14/reference/bson/mapping/conventions/