0

EF Core 5

I have a classic setup of one-to-many navigation in EF Core code first.

One site <--> Many assets

I'll post the code just so that you see it with your own eyes :

public class Site : IAggregateRoot
{
    public Guid Id { get; private set; }

    private readonly List<Asset> _assets = new List<Asset>();
    public IReadOnlyCollection<Asset> Assets => _assets;

    public Site(Guid id) { Id = id; }
}


public class Asset : IAggregateRoot
{
    public Guid Id { get; private set; }

    public Site? Site { get; private set; }
    public Guid? SiteId { get; private set; }


    public Asset(Guid id) { Id = id; }

    public Asset(Guid id, Guid siteId) : this(id: id)
    {
        SiteId = siteId;
    }

    public void SetSite(Guid? siteId)
    {
        SiteId = siteId;
    }
}

Asset model builder :

        builder.ToTable("Asset");

        BaseModelHelper.InitBaseModel(builder);

        builder
            .HasOne(x => x.Site)
            .WithMany(x => x.Assets)
            .HasForeignKey(x => x.SiteId)
            .OnDelete(DeleteBehavior.Restrict);

Site model builder:

        builder.ToTable("Site");

        BaseModelHelper.InitBaseModel(builder);


        builder.Metadata
            .FindNavigation(nameof(Site.Assets))
            .SetPropertyAccessMode(PropertyAccessMode.Field);

It's managed with repositories, which in turn rely on Unit of Work design pattern:

public class AppDbContext : DbContext, IUnitOfWork
{
    ...
    public DbSet<Site> Sites { get; set; } = null!;
    public DbSet<Asset> Assets { get; set; } = null!;
    ...
}

then

public class SiteRepository
{
    private readonly AppDbContext _dbContext;
    public IUnitOfWork UnitOfWork => _dbContext;

    public SiteRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void UnloadFromContext(Site site)
    {
        _dbContext.Entry(site).State = EntityState.Detached;
    }

    public async Task ReloadAsync(Site site)
    {
        await _dbContext.Entry(site).ReloadAsync();
    }

    public async Task<Site?> GetWithAssetsAsync(Guid id)
    {
        return await _dbContext.Sites
            .Include(x => x.Assets)
            .FirstOrDefaultAsync(x => x.Id == id);
    }


    public async Task AddAsync(Site site)
    {
        await _dbContext.Sites.AddAsync(site);
    }
}

and

public class AssetRepository : IAssetRepository
{
    private readonly AppDbContext _dbContext;
    public IUnitOfWork UnitOfWork => _dbContext;
    public AssetRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    ...
    //The rest is similar to SiteRepository
 }
    

===================

Now I'm just trying to refresh the navigation after changes to the database. I know that the changes occur because I see it through SQL querying while the app is halted using a breakpoint. After querying, I let the app continue and do the refresh. yet site.Assets remains empty...

        var site = new Site(id);
        await sitesRepo.AddAsync(site);
        await sitesRepo.UnitOfWork.SaveChangesAsync();

then

            var assets = await assetsRepo.GetOrCreateAsync(assetIds);
            await assetsRepo.UnitOfWork.SaveChangesAsync();

At this point everything I need has been created. A site and some assets. I now want to assign some assets to the site. I do it by changing the navigation from the asset's side because it seems easier that way : I simply change asset.SiteId.

            var assetsToAdd = assets.Where(whatever condition);
            foreach (var asset in assetsToAdd)
            {
                asset.SetSite(siteId);
            }
            await assetsRepo.UnitOfWork.SaveChangesAsync();
            await sitesRepo.UnitOfWork.SaveChangesAsync();

At this point I see in the db that the site DOES have assets. All asset.SiteId are correct.

Later in the code I need to start wrking from the site's side of the navigation. Since some assets mught have been added, I need to make sure that the site.Assets navigation now contains those new assets. Therefore I try to "reload" that navigation, since EF doesn't seem to do it by itself :

            //My pitiful attempts : I've read that detaching an entity before loading it reloads the navigation...
            sitesRepo.UnloadFromContext(site); // This does _dbContext.Entry(site).State = EntityState.Detached;
            
            //await sitesRepo.ReloadAsync(site); //I tried that as an alternative...

            

            var updatedSite = await sitesRepo.GetWithAssetsAsync(siteId); //This does the Include
            
            //updatedSite is not null, I checked
            var updatedAssets = updatedSite?.Assets.ToList() ?? new List<Asset>(); //Always empty :'-(
            return updatedAssets;

I've looked at those articles but it still eludes me

Reload navigation property EF

How to refresh an Entity Framework Core DBContext?

Reload an entity and all Navigation Property Association- DbSet Entity Framework

Why won't it reload the damn navigation???

EDIT : I've detailed the repos to show how the dbContext is managed.

jeancallisti
  • 1,046
  • 1
  • 11
  • 21
  • Why should it reload anything? That's not how ORMs in general, or EF in particular, work. While EF (and other ORMs) have ways to explicitly load entities, your code doesn't use any of them. Attaching a detached object means just that - attaching, not loading or reloading. There's no `ReloadAsync` in EF so what does `ReloadAsync` do? Besides, the code shows you've used the "generic repositoy" **anti**pattern. A DbSet is already a single-entity repository, a DbContext already is a Unit-of-Work that works in a disconnected way. – Panagiotis Kanavos Apr 13 '21 at 12:12
  • The question doesn't explain what the actual problem is, or what should be reloaded, but the way `SaveChanges` is used breaks UoW. A DbContext caches all changes and persists them all in a single transaction when `SaveChanges` is called. Not only is there no need to call `SaveChanges` multiple times, it's actually a *serious bug* that breaks UoW and forces you to use explicit transactions. Besides, that `SaveChanges` could easily perform 100 DELETEs and 42 UPDATEs along with that single INSERT, precisely because it *is* a UoW and caches all changed entities – Panagiotis Kanavos Apr 13 '21 at 12:17
  • I can clarify what ReloadAsync does. Also we do use Unit of Works. I will edit my post and expose more code. But in a nutshell my issue is simple: You ask "why should it reload?". I'm not _expecting_ it to reload. I'm just failing to reload ... at all! I fail to reflect the changes to the db in my C# code. – jeancallisti Apr 13 '21 at 12:21
  • Long story short, the navigation properties are wrong because the repo antipattern broke them. There's very little EF code in this question, so it's hard to guess what's going on, or even what you need to reload - you're adding new properties, so why reload anything??? The actual navigation properties are missing, or hidden behind the "repositories". What happens if you *remove all the repositories*, and only use `_dbContext.SaveChangesAsync` at the end? PS `sitesRepo.UnitOfWork.SaveChangesAsync();` does nothing at all, unless it uses a *different* DbContext, which would be another problem – Panagiotis Kanavos Apr 13 '21 at 12:25
  • `Also we do use Unit of Works.` no you don't. Unless you added yet another explicit transaction. `SaveChanges` is what commits the UoW. That's how EF works. Anything else completely breaks it. And `I'm not expecting it to reload. I'm just failing to reload ... at all!` is self-contradictory: you're adding new items, what are you expecting to "reload"? Are you trying to read some computed properties like the ID perhaps? It's `SaveChanges` that takes care of that. Assuming the property is marked as `Generated` – Panagiotis Kanavos Apr 13 '21 at 12:26
  • [This possibly duplicate question](https://stackoverflow.com/questions/5212751/how-can-i-retrieve-id-of-inserted-entity-using-entity-framework) shows that retrieving the generated IDs is as simple as just reading them after `SaveChangesAsync`. The code you posted doesn't mark `Asset.Id` or `Site.Id` as a database-generated key though, so EF won't know to retrieve any generated values. It will simply use the values that were provided – Panagiotis Kanavos Apr 13 '21 at 12:30
  • @PanagiotisKanavos I think the link you posted has nothing to do with my question, but I could be wrong. – jeancallisti Apr 13 '21 at 12:32
  • You haven't explained what the problem is yet. You show custom methods and calls that are only indirectly related to EF Core. Clean up the code first. Remove all the repositories. Use EF Core as it's meant to be used. Fix the problem with the clean code first. *Only then* think about using other abstractions to simplify things. You'll find the repositories you really need have **nothing** to do with what you have now. – Panagiotis Kanavos Apr 13 '21 at 12:33
  • Now that I've edited my post and presented how we use Unit of Work, I'll explain again what the problem is : a Site has some Assets. I add an Asset in the navigation collection, then save the unit of work. Later, in another function, how do I re-query that Site WITH its assets? Currently, re-querying it doesn't show the new Asset in the navigation even though it's there in the db. – jeancallisti Apr 13 '21 at 12:35
  • The code you posted verifies the misuse of EF. You're calling `SaveChanges` on the *same* DbContext twice. Why? And once again, what do you expect to "reload" ? – Panagiotis Kanavos Apr 13 '21 at 12:36
  • Ignore the repeated Saves. That's an act of desperation. Even though calling save several times is not efficient, it shouldn't break the flow... I expect the NEW asset to APPEAR in the navigation. I added an Asset to a Site and it's not there. That's not correct. I want it to be there. I don't understand how I could explain it better. – jeancallisti Apr 13 '21 at 12:39
  • To which navigation? `Assets`? You never add anything there. That **computed property** is written in a way that would prevent any other class from adding anything. EF Core can't guess that it needs to set the `_assets` private field to load the `Assets` property. – Panagiotis Kanavos Apr 13 '21 at 12:41
  • What's the point of all this code? The only thing it does is complicate things to the point that nobody understands what's going on. That's not EF's fault – Panagiotis Kanavos Apr 13 '21 at 12:43
  • I don't add the asset to site.Assets explicitly, but since I change the navigation from the asset's side (by setting asset.SiteId), then there must be a way to reflect that change when looking at the navigation from the OTHER side, i.e. the site's side : site.Assets. – jeancallisti Apr 13 '21 at 12:43
  • OK apparently we don't understand each other at all. Thanks for the time you spent on this. – jeancallisti Apr 13 '21 at 12:45
  • My EF code works. Yours doesn't. And doesn't look like any EF example or tutorial either. And I already found dozens of problems. You can't be the first person to try to use read only properties or Unit-of-Work with EF in the last 10 years. So why assume EF is at fault? – Panagiotis Kanavos Apr 13 '21 at 12:47
  • I'm not assuming that EF is at fault. I'm trying to understand exactly what I'm doing wrong. You pointed out some clumsiness but I don't see what bit exactly would specifically break EF. – jeancallisti Apr 13 '21 at 12:53

0 Answers0