Consider this aggregate root...
class Contact
{
ICollection<ContactAddress> Addresses { get; set; }
ICollection<ContactItem> Items { get; set; }
ICollection<ContactEvent> Events { get; set; }
}
...which I have used like so...
class Person
{
Contact ContactDetails { get; set; }
}
How do I eager load all of the collections with the contact?
I tried this...
Context
.Set<Person>()
.Include(o => o.ContactDetails)
.ThenInclude(o => o.Addresses)
.ThenInclude(????)
. ...
I've also tried this...
Context
.Set<Business>()
.Include(o => o.ContactDetails.Addresses)
.Include(o => o.ContactDetails.Events)
.Include(o => o.ContactDetails.Items)
On a somewhat related note, is it possible to express what should be returned as part of an aggregate root using fluent configuration?
The ThenInclude pattern allows you to specify a path from the root to a single leaf, hence in order to specify a path to another leaf, you need to restart the process from the root by using the Include method and repeat that for each leaf.
For your sample it would be like this:
Context.Set<Person>()
.Include(o => o.ContactDetails).ThenInclude(o => o.Addresses) // ContactDetails.Addresses
.Include(o => o.ContactDetails).ThenInclude(o => o.Items) // ContactDetails.Items
.Include(o => o.ContactDetails).ThenInclude(o => o.Events) // ContactDetails.Events
...
Reference: Loading Related Data - Including multiple levels
Related
We are building an application using .NET 6 and EF Core 6 with an existing SQL Server database. We are using the database first approach and running the Scaffold-DbContext tool we were able to generate the dbcontex class. Everything works fine, a part for a parent child relation between two tables:
The scaffold tool, for the above tables generated the following two classes:
public partial class TreeNode
{
public TreeNode()
{
TreeNodeHierarchyChildren = new HashSet<TreeNodeHierarchy>();
TreeNodeHierarchyParents = new HashSet<TreeNodeHierarchy>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public bool IsLeaf { get; set; }
public int? OrganisationId { get; set; }
public bool IsDeleted { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyChildren { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyParents { get; set; }
}
public partial class TreeNodeHierarchy
{
public int Id { get; set; }
public int ParentId { get; set; }
public int ChildId { get; set; }
public virtual TreeNode Child { get; set; }
public virtual TreeNode Parent { get; set; }
}
And in the dbcontext class the following mapping:
modelBuilder.Entity<TreeNode>(entity =>
{
entity.ToTable("TreeNode");
entity.Property(e => e.Code).HasMaxLength(100);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
});
modelBuilder.Entity<TreeNodeHierarchy>(entity =>
{
entity.ToTable("TreeNodeHierarchy");
entity.HasOne(d => d.Child)
.WithMany(p => p.TreeNodeHierarchyChildren)
.HasForeignKey(d => d.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
entity.HasOne(d => d.Parent)
.WithMany(p => p.TreeNodeHierarchyParents)
.HasForeignKey(d => d.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
Here is the issue, when I write the following:
var nodes = _context.TreeNodes.Include(th => th.TreeNodeHierarchyChildren)
.Where(tn => tn.IsLeaf)
.....
it loads the child but not the parent.
This relation works properly in the current application (.net 4.7) using LINQ to SQL.
Am I missing something?
Updated
as suggested from #SpruceMoose, I included also the TreeNodeHierarchyParents property in the query but it didn't fix the issue.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
Updated #2
I applied the mapping suggested from #Dave which in my opinion it makes sense (at the end the relation is like the Windows folders/files system).
Anyway there is still something that's not working properly. When I debug the following code:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.ToList();
I still see that the parent has not been loaded
Updated #3
I applied the change to the query as suggested from #Moho
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
.ToList();
and finally we got the Parent value
Now we are missing the last step, the parents of a parent
You need to explicitly (eagerly) load the Parent elements by using an Include() on the TreeNodeHierarchyParents navigation property (as you are currently for the TreeNodeHierarchyChildren navigation property).
Change your linq query to the following:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.....
I think your relationship mapping is wrong. You say one child has many children and one parent has many parents. It should be one child has many parents, and one parent has many children.
I think it's also a good idea to define these kinds of relationships on both sides, so that if you get something wrong it shows up as an error faster. Note also that I think some of these statements would already be the default.
Also, important, note that I think you need to use Nullable Reference Types to indicate nullability. Anything that is supposed to be nullable should have a ? on its type name in the entity types. Though I think it's possible you should cascade delete, not set null. It depends how your model works.
Something like this, though I can't guarantee compilation:
modelBuilder.Entity<TreeNode>(tnb => {
tnb.ToTable("TreeNode");
tnb.Property(tn => tn.Code).HasMaxLength(100);
tnb.Property(tn => tn.Name).IsRequired().HasMaxLength(255);
tnb
.HasMany(tn => tn.TreeNodeHierarchyParents)
.WithOne(tnh => tnh.Child);
tnb
.HasMany(tn => tn.TreeNodeHierarchyChildren)
.WithOne(tnh => tnh.Parent);
});
modelBuilder.Entity<TreeNodeHierarchy>(tnhb => {
tnhb.ToTable("TreeNodeHierarchy");
tnhb
.HasOne(tnh => tnh.Child)
.WithMany(tn => tn.TreeNodeHierarchyParents)
.HasForeignKey(tnh => tnh.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
tnhb
.HasOne(tnh => tnh.Parent)
.WithMany(tn => tn.TreeNodeHierarchyChildren)
.HasForeignKey(tnh => tnh.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
One thing you can do to try to make sure your model definition is correct is to create an empty second database with it and compare its model against the real one, and then keep fine tuning it until you get it right.
Regarding Update #2:
You’re eager loading the relationship entities of type TreeNodeHierarchy but you are not eager loading the TreeNode entities they reference. You need to add .ThenInclude calls to do so.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
The Child property in your example is populated in your current query because the TreeNode entity is loaded by your base query and EF Core will automatically hook it up to relevant navigation properties in other tracked entities. Thus, any “parent” that is not IsLeaf (nor any “child” that is not IsLeaf) is not loaded without the additional ThenInclude.
Another (not recommended) alternative is to enable lazy loading.
I have a .NET Core 2.1 library that is leveraging Entity Framework Core v.2.2.4 with a MySQL database. I am using the code first model. I have service classes that depend on the SoarDataContext class seen below.
When I try to eagerly-load multiple-level nested entities by using the .Include() method in the way suggested in the docs, I get the following error:
"Error fetching productions: The Include property lambda expression 'p => {from Participant m in p.Participants select [m].EmergencyContacts}' is invalid. The expression should represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, specify an explicitly typed lambda parameter of the target type, E.g. '(Derived d) => d.MyProperty'. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393."
Here is the service method:
public async Task<IEnumerable<Production>> GetProductions()
{
var productions = _context.Productions
.Include(p => p.AuditionTimes)
.Include(p => p.Participants.Select(m => m.EmergencyContacts))
.ToList();
return await Task.FromResult(productions);
}
My intent in the above code is to get all Productions back with all AuditionTimes "pre-fetched" and all Participants pre-fetched with all of their EmergencyContacts pre-fetched as well. Because this is the suggestion from the docs and it's blowing up the way it is seems contradictory.
FWIW, here is the class definition for my DataContext class:
using Microsoft.EntityFrameworkCore;
using Models;
namespace DataAccess
{
public class SoarDataContext : DbContext
{
public SoarDataContext(DbContextOptions<SoarDataContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
public DbSet<Participant> Participants { get; set; }
public DbSet<ParticipantType> ParticipantTypes { get; set; }
public DbSet<Production> Productions { get; set; }
public DbSet<EmergencyContact> EmergencyContacts { get; set; }
public DbSet<AuditionTime> AuditionTimes { get; set; }
//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//{
//}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasIndex(u => u.Username)
.IsUnique();
modelBuilder.Entity<Participant>()
.HasOne(p => p.ParticipantType);
modelBuilder.Entity<Participant>()
.HasIndex(p => new { p.FirstName, p.LastName, p.ProductionId })
.IsUnique();
modelBuilder.Entity<ParticipantType>()
.HasIndex(pt => pt.Type)
.IsUnique();
modelBuilder.Entity<Production>()
.HasMany(p => p.Participants);
modelBuilder.Entity<Production>()
.HasMany(p => p.AuditionTimes);
modelBuilder.Entity<Production>()
.HasIndex(p => new { p.Name, p.SeasonName, p.SeasonYear, p.AgeRange })
.IsUnique();
modelBuilder.Entity<EmergencyContact>()
.HasOne(p => p.Participant);
modelBuilder.Entity<AuditionTime>()
.HasIndex(a => new { a.ProductionId, a.StartTime, a.EndTime })
.IsUnique();
modelBuilder.Entity<AuditionTime>()
.HasOne(a => a.Production);
}
}
}
What am I missing?
EDIT:
When trying the answers being suggested, I had a hard time with Visual Studio 2019's IntelliSense. For anyone running into IntelliSense suggesting you're syntax is invalid, try to compile anyways. I found it particularly confusing that the suggested answers wouldn't even compile, but it turns out IntelliSense was just giving me inaccurate errors. As mentioned in the docs:
Current versions of Visual Studio offer incorrect code completion options and can cause correct expressions to be flagged with syntax errors when using the ThenInclude method after a collection navigation property. This is a symptom of an IntelliSense bug tracked at https://github.com/dotnet/roslyn/issues/8237. It is safe to ignore these spurious syntax errors as long as the code is correct and can be compiled successfully.
The initial documentation you linked to is for Entity Framework 6 while the error message links to Entity Framework Core.
Reference Loading Related Data: Including multiple levels
You can drill down through relationships to include multiple levels of related data using the ThenInclude method.
var productions = _context.Productions
.Include(p => p.AuditionTimes)
.Include(p => p.Participants)
.ThenInclude(p => p.EmergencyContacts)
.ToList();
Service method could be refactored to
public async Task<IEnumerable<Production>> GetProductions() {
var productions = await _context.Productions
.Include(p => p.AuditionTimes)
.Include(p => p.Participants)
.ThenInclude(p => p.EmergencyContacts)
.ToListAsync();
return productions;
}
Sample models.
public class Root
{
public string Id { get; private set; }
public ICollection<Child> Children { get; private set; }
}
public class Child
{
public string Id { get; private set; }
public string RootId { get; private set; }
public string Code { get; private set; }
public string Name { get; private set; }
}
Constraints.
Child has the RootId and Code property as its unique key. This means that each Root object is only allowed to have as many Child objects as long as no two or more Child contains the same code.
Sample query
Get all Root records with Child that have Code equals A100.
Sample List Data Containing two Root objects
Root1 with 2 children, one having a code A100 and the other A200.
Root2 with 2 children, one having a code A100 and the other A500.
The current query that I am doing right now is get all the Root records first along with all their children. Then, iterate each of the records and remove all of its children that doesn't have the same code that I am querying. The problem with this approach is when the database grows, it will have an impact on this method since I am retrieving all children when all I need is one for each Root objects.
Sample code
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.ToList();
foreach (var root in records)
{
foreach (var child in root.Children)
{
if (!child.Code == "A100")
{
root.Children.Remove(child);
}
}
}
My models have their property setters set to private following DDD principles. So I cannot do linq projections using the Select() command like the following.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root{...})
.ToList();
Using the constructor is also not ideal in my case because I am setting the state of each object to Created during instantation as part of the design of each model.
Edit 1
I could use the constructor in the LINQ projection using Select() but my problem is, in all of my models, there is a property called State where I update in various points in my model depending on what occurred. In the constructor part, I update it to a Create state to imply the fact the a new model was created. So if I am going to create a constructor just so I could create an instance of the model from the database, that would lead to confusion because I am just retrieving an already existing record from the database and if I am going to use the constructor, the code, during the instantiation will mark the model as Created which is not what I want because it will create a new meaning in my design.
Edit 2
My apologies for not making myself clear enough. My problem is on this part of the query.
Part 1.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.ToList();
So I won't need to arrive on this part.
Part 2
foreach (var root in records)
{
foreach (var child in root.Children)
{
if (!child.Code == "A100")
{
root.Children.Remove(child);
}
}
}
Now based on the constraints I mentioned.
Constraint 1. Not using public setters, so I cannot use this.
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root{...})
.ToList();
Constaint 2. Not using constructor
var records = context.Roots
.Include(x => x.Children)
.Where(x => x.Children.Any(y => y.Code == "A100"))
.Select(x => new Root(...))
.ToList();
The bottom line is, is there a query that I can use or any other method get the records I want, straight from the database without doing the second part of the query?
Try traditional LINQ so you will not more need to remove children manually and project your query result to the anonymous object.
var result = (from root in context.Roots.Include(x => x.Children)
from child in root.Children
where child.Code == "A100"
select new
{
Id = root.Id,
Children = child
}).ToList();
Unless you have some kind of sorting in your data storage that you can use you still have to "retrieve" items to look at them. And if you want a copy of your data with the result instead of modifying your context data, you need some kind of cloning. So in my opinion - considering your constraints - it is best to only keep references on the resulting Rootand Child items:
var l = new List<Tuple<Root, Child>>();
foreach(var p in context.Roots.Include(x => x.Children))
{
foreach(var c in p.Children)
{
if(c.Code == "A100")
{
l.Add(Tuple.Create(p, c));
break;
}
}
}
That way, you only look at the children and the root items once, and only check children until you found your item. The resulting list of tuples contain references to your respective Root and Child items without modifying them, so don't use the Children property of your referenced Root items.
This question already has answers here:
EF: Include with where clause [duplicate]
(5 answers)
Closed 2 years ago.
I have seen a few answers to similar questions, however I cannot seem to work out how to apply the answer to my issue.
var allposts = _context.Posts
.Include(p => p.Comments)
.Include(aa => aa.Attachments)
.Include(a => a.PostAuthor)
.Where(t => t.PostAuthor.Id == postAuthorId).ToList();
Attachments can be uploaded by the Author (type Author) or Contributor (type Contributor). What I want to do, is only get the Attachments where the owner of the attachment is of type Author.
I know this doesn't work and gives an error:
.Include(s=>aa.Attachments.Where(o=>o.Owner is Author))
I've read about Filtered Projection here
EDIT - link to article:
: http://blogs.msdn.com/b/alexj/archive/2009/10/13/tip-37-how-to-do-a-conditional-include.aspx,
but I just can't get my head around it.
I don't want to include the filter in the final where clause as I want ALL posts, but I only want to retrieve the attachments for those posts that belong to the Author.
EDIT 2: - Post schema requested
public abstract class Post : IPostable
{
[Key]
public int Id { get; set; }
[Required]
public DateTime PublishDate { get; set; }
[Required]
public String Title { get; set; }
[Required]
public String Description { get; set; }
public Person PostAuthor { get; set; }
public virtual ICollection<Attachment> Attachments { get; set; }
public List<Comment> Comments { get; set; }
}
EF Core 5.0 is introducing Filtered Include soon.
var blogs = context.Blogs
.Include(e => e.Posts.Where(p => p.Title.Contains("Cheese")))
.ToList();
Reference: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#filtered-include
From the link you posted I can confirm that trick works but for one-many (or many-one) relationship only. In this case your Post-Attachment should be one-many relationship, so it's totally applicable. Here is the query you should have:
//this should be disabled temporarily
_context.Configuration.LazyLoadingEnabled = false;
var allposts = _context.Posts.Where(t => t.PostAuthor.Id == postAuthorId)
.Select(e => new {
e,//for later projection
e.Comments,//cache Comments
//cache filtered Attachments
Attachments = e.Attachments.Where(a => a.Owner is Author),
e.PostAuthor//cache PostAuthor
})
.AsEnumerable()
.Select(e => e.e).ToList();
Remove the virtual keyword from your Attachments navigation property to prevent lazy loading:
public ICollection<Attachment> Attachments { get; set; }
First method: Issue two separate queries: one for the Posts, one for the Attachments, and let relationship fix-up do the rest:
List<Post> postsWithAuthoredAttachments = _context.Posts
.Include(p => p.Comments)
.Include(p => p.PostAuthor)
.Where(p => p.PostAuthor.Id == postAuthorId)
.ToList();
List<Attachment> filteredAttachments = _context.Attachments
.Where(a => a.Post.PostAuthor.Id == postAuthorId)
.Where(a => a.Owner is Author)
.ToList()
Relationship fixup means that you can access these filtered Attachments via a Post's navigation property
Second method: one query to the database followed by an in-memory query:
var query = _context.Posts
.Include(p => p.Comments)
.Include(p => p.PostAuthor)
.Where(p => p.PostAuthor.Id == postAuthorId)
.Select(p => new
{
Post = p,
AuthoredAttachments = p.Attachments
Where(a => a.Owner is Author)
}
);
I would just use the anonymous type here
var postsWithAuthoredAttachments = query.ToList()
or I would create a ViewModel class to avoid the anonymous type:
List<MyDisplayTemplate> postsWithAuthoredAttachments =
//query as above but use new PostWithAuthoredAttachments in the Select
Or, if you really want to unwrap the Posts:
List<Post> postsWithAuthoredAttachments = query.//you could "inline" this variable
.AsEnumerable() //force the database query to run as is - pulling data into memory
.Select(p => p) //unwrap the Posts from the in-memory results
.ToList()
You can use this implementation of an extension method (eg.) Include2(). After that, you can call:
_context.Posts.Include2(post => post.Attachments.Where(a => a.OwnerId == 1))
The code above includes only attachments where Attachment.OwnerId == 1.
try this
var allposts = _context.Posts
.Include(p => p.Comments)
.Include(a => a.PostAuthor)
.Where(t => t.PostAuthor.Id == postAuthorId).ToList();
_context.Attachments.Where(o=>o.Owner is Author).ToList();
For net core
https://learn.microsoft.com/ru-ru/ef/core/querying/related-data/explicit
var allposts = _context.Posts
.Include(p => p.Comments)
.Include(a => a.PostAuthor)
.Where(t => t.PostAuthor.Id == postAuthorId).ToList();
_context.Entry(allposts)
.Collection(e => e.Attachments)
.Query()
.Where(e=> e.Owner is Author)
.Load();
it makes 2 query to sql.
Lambda in Include() may only point to a property:
.Include(a => a.Attachments)
.Include(a => a.Attachments.Owner);
Your condition doesn't makes sense for me because Include() means join and you either do it or not. And not conditionally.
How would you write this in raw SQL?
Why not just this:
context.Attachments
.Where(a => a.Owner.Id == postAuthorId &&
a.Owner.Type == authorType);
?
Assuming "a" being of type "YourType", a conditonal include could be solved by using a method extension, e.g.
public static class QueryableExtensions
{
public static IQueryable<T> ConditionalInclude<T>(this IQueryable<T> source, bool include) where T : YourType
{
if (include)
{
return source
.Include(a => a.Attachments)
.Include(a => a.Attachments.Owner));
}
return source;
}
}
... then just use this like you are using .Include, e.g.
bool yourCondition;
.ConditionalInclude(yourCondition)
i have 3 tables in my database:
Projects (id, name)
Tags (id, name)
ProjectsTagss (id, projectId, tagid)
As you can see the ProjectsTags table is a bridge table
here is my fluent nhibernate mapping
ProjectMap.cs:
Map(x => x.Name).Not.Nullable();
HasMany(x => x.ProjectsTags).AsBag().Inverse()
.Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
ProjectsTagsMap.cs:
References(x => x.Project).Not.Nullable();
References(x => x.Tag).Not.Nullable();
TagMap.cs:
Map(x => x.Name).Not.Nullable();
As you can see, i historically didn't have the Tag table linked to anything else. I now need to generate a report to show Tag and how often that tag is used so i need to join from Tag to ProjectsTag. i tried adding this line into the tagsmap:
HasMany(x => x.ProjectsTags).AsBag().Inverse()
.Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
but when i go to update the name on a tag object and commit, i get this error:
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance
can anyone see anything wrong with what i added that would be causing this nhibernate exception when i simply update the Tag table. Again my goal is to be able to do something like:
Tag.ProjectTags.Count();
Here is some additional code as requested:
my Tag Class:
public class Tag
{
public virtual IList<ProjectTag> ProjectTags { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}
Somewhere in your code, you should have dereferenced the original collection on your Project domain. I suspect that your code goes like this:
var project = Session.Get<Project>();
project.ProjectsTags = new List<ProjectsTags> { someProjectsTagsInstance };
Session.Save(project);
If this is the case, you should do this instead:
var project = Session.Get<Project>();
project.ProjectsTags.Clear();
project.ProjectsTags.Add(someProjectsTagsInstance);
Session.Save(project);
While a collection is not modified, NH can still think that it is. Something like this could be caused by a ghost update. From NHibernate 3.0 Cookbook, Jason Dentler (page 184): "As part of automatic dirty checking, NHibernate compares the original state of an entity to
its current state. An otherwise unchanged entity may be updated unnecessarily because a
type conversion caused this comparison to fail".
Ghost update of collection can be caused by code that looks like this:
public class Tag
{
private IList<ProjectTag> projectsTags;
public virtual IEnumerable<ProjectTag> ProjectsTags
{
get
{
return new ReadOnlyCollection<ProjectTag>(projectsTags);
}
set
{
projectsTags = (IList<ProjectTag>)value;
}
}
}
ProjectsTags property returns the collection in readonly wrapper, so client code cannot add or remove elements to/from the collection.
The error will appear even when name of a tag is not changed:
private void GhostTagUpdate(int id)
{
using (var session = OpenSession())
{
using (var transaction = session.BeginTransaction())
{
var tag = session.Get<Tag>(id);
transaction.Commit();
}
}
}
ProjectsTags collection should be mapped with CamelCaseField access strategy to avoid ghost updated:
HasMany(x => x.ProjectsTags)
.Access.CamelCaseField()
.AsBag().Inverse().Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
Anyway...
Your association seems to be diabolically complex. If ProjectsTags table should contains only id of tag and id of project, then it would be simpler to use FNH many-to-many bidirectional mapping:
public class Tag2Map : ClassMap<Tag2>
{
public Tag2Map()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Projects)
.AsBag()
.Cascade.None()
.Table("ProjectsTags")
.ParentKeyColumn("TagId")
.ChildKeyColumn("ProjectId");
}
}
public class Project2Map : ClassMap<Project2>
{
public Project2Map()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Tags)
.AsBag()
.Cascade.None()
.Inverse()
.Table("ProjectsTags")
.ParentKeyColumn("ProjectId")
.ChildKeyColumn("TagId");
}
}
Now there is no need for ProjectTag entity in the model. The count of how many times is given tag used can be retrieved in two ways:
Direct way: tag.Projects.Count() - but it retrieves all projects from database.
Query way:
var tag = session.Get<Tag2>(tagId);
var count = session.Query<Project2>().Where(x => x.Tags.Contains(tag)).Count();