I'm having an issue with EFCore 2.0 where its impossible to properly delete dependent entities when updating the prinicipal entity. Here's my code:
Mapping
public class BlueprintMap : IEntityTypeConfiguration<Blueprint>
{
public void Configure(EntityTypeBuilder<Blueprint> builder)
{
builder.ToTable("blueprint").HasKey(t => t.Id);
builder.Property(x => x.Id).HasColumnName("blueprint_id").ValueGeneratedOnAdd();
builder.HasMany<Objective>().WithOne(x => x.Blueprint).OnDelete(DeleteBehavior.Cascade);
}
}
public class ObjectiveMap : IEntityTypeConfiguration<Objective>
{
public void Configure(EntityTypeBuilder<Objective> builder)
{
builder.ToTable("objective").HasKey(x => x.Id);
builder.Property(x => x.Id).HasColumnName("objective_id").ValueGeneratedOnAdd();
builder.HasOne(x => x.Blueprint).WithMany(y => y.Objectives).HasForeignKey("blueprint_id");
}
}
Update WebApi Call
Get from context
var blueprint = await blueprintContext.Blueprints
.Where(x => id == x.Id)
.Include(x => x.Objectives)
.SingleOrDefaultAsync();
Remove objectives from blueprint entity (Objectives is ICollection, the dependent entity
blueprint.Objectives.Remove(objective);
// Get a new context
var blueprintContext = await _blueprintContextFactory.CreateContext();
Blueprint entity AND Blueprint from context have 0 objectives
int x = await blueprintContext.SaveChangesAsync();
Get WebApi Call
Get from context
return await blueprintContext.Blueprints
.Where(x => id == x.Id)
.Include(x => x.Objectives)
.SingleOrDefaultAsync();
blueprint has 2 objectives again.
Stupid mistake. The first context tracks the objectives being removed. When a new context is generated to save the changes, it doesn't know about the objectives being removed so they remain and regenerate. In other words, the Update request is using multiple contexts instead of just 1 per request and this is unacceptable by EF Core's design.
Related
According to this documentation page there is a neat way to count nested collection elements without actually fetching them. Great. But the example is for a single item.
Of course, I could iterate and do this in post-processing. But that's not neat.
And here could something like Automapper come into play. It has a ProjectTo extension for IQueryable which can be parameterized (in quite a weird way imho).
Well, I can't get it working. Let's say I have a Client entity that has associated Device entities, and I want to set the DeviceCount property of the target.
By combining the examples from the two sources, something like this could work:
_configuration = new MapperConfiguration(cfg =>
{
SMPContext ctx = null; ///why not an extra property in conf?
cfg.CreateProjection<Client, Models.ViewModels.Client>()
.ForMember(d => d.DeviceCount, conf => conf.MapFrom(s => ctx.Entry(s).Collection(c => c.Devices).Query().Count()));
});
}
...
public async Task<IActionResult> Index()
{
var res = await
_context
.Clients.ProjectTo<SMP.Models.ViewModels.Client>(_configuration, new { ctx = _context })
.ToListAsync();
...
I was experimenting with this too:
public async Task<IActionResult> Index()
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.CreateProjection<Client, Models.ViewModels.Client>()
.ForMember(d => d.DeviceCount, conf => conf.MapFrom(s => _context.Entry(s).Collection(c => c.Devices).Query().Count()));
});
var res = await
_context
.Clients.ProjectTo<SMP.Models.ViewModels.Client>(configuration)
.ToListAsync();
...
Either way, I get such an exception, which is quite understandable:
InvalidOperationException: The LINQ expression
'___context_0.Entry(NavigationTreeExpression Value:
EntityReference: Client Expression: c).Collection(c =>
c.Devices).Query() .Count()' could not be translated.
I have tried using Map instead of ProjectTo, but no luck either, as IMapper.Map().AfterMap passes the entire enumerable as parameter instead of per item.
Is there any neat solution?
It looks like there is a simple and neat solution with AutoMapper after all: CreateProjection is smart enough to create a performant SQL mapping in such a case:
builder.Services.AddAutoMapper(config =>
config.CreateProjection<SMP.Models.DataModels.Client, SMP.Models.ViewModels.Client>()
.ForMember(d => d.DeviceCount, o=> o.MapFrom(s => s.Devices.Count))
);
Using it is simple as well:
var res = await _context.Clients
.ProjectTo<Models.ViewModels.Client>(_mapper/*injected IMapper*/.ConfigurationProvider).ToListAsync();
The generated SQL query looks like this:
How can I clean up my entity 'includes' to reuse the same set of statements? I'm trying to reuse a set of Includes while staying DRY.
Before
_context.Accounts
.Include(x=> x.Status)
.Include(x=> x.Type)
.Include(x=> x.Phones)
.Include(x=> x.Users)
.Include(x=> x.Admins)
After:
_context.Accounts.CustomIncludes()
Try this:
public static class DataExtensions
{
public static Microsoft.EntityFrameworkCore.Query.IIncludableQueryable<Account, List<Admin>> CustomIncludes(this DbSet<Account> accounts)
{
return accounts
.Include(p => p.Status)
.Include(p => p.Type)
.Include(p => p.Phones)
.Include(p => p.Users)
.Include(p => p.Admins);
}
}
Then you can say
context.Accounts.CustomIncludes();
I have the following repository method:-
public AccountDefinition GetCustomer2(int id)
{ var c = entities.AccountDefinitions.Where(p => p.ORG_ID == id)
.Include(a => a.SDOrganization)
.Include(a2 => a2.SiteDefinitions)
.Include(a3 => a3.SDOrganization.AaaPostalAddresses)
.Include(a4 => a4.SiteDefinitions.Select(a5 => a5.DepartmentDefinitions.Select(a6 => a6.SDUsers.Select(a7 => a7.AaaUser.AaaContactInfoes)))).SingleOrDefault();
return c; }
But when I comment some code I found that the generated SQL statement when calling the Action method will be the same:-
public AccountDefinition GetCustomer2(int id)
{ var c = entities.AccountDefinitions.Where(p => p.ORG_ID == id)
// .Include(a => a.SDOrganization)
// .Include(a2 => a2.SiteDefinitions)
.Include(a3 => a3.SDOrganization.AaaPostalAddresses)
.Include(a4 => a4.SiteDefinitions.Select(a5 => a5.DepartmentDefinitions.Select(a6 => a6.SDUsers.Select(a7 => a7.AaaUser.AaaContactInfoes)))).SingleOrDefault();
return c; }
So does this mean when I navigate to a nested Navigation property , then EF will automatically retrieve parent navigation properties also? . so for example when i write .Include(a3 => a3.SDOrganization.AaaPostalAddresses) , then there is no need to write .Include(a => a.SDOrganization)
In a nutshell yes, it has to otherwise there is no way for you to traverse to that navigation property
EG, the only way to access AccountDefinition.SDOrganization.AaaPostalAddresses is if SDOrganization is not null.
Having said that my personal preference is to make this intermediate inclusion explicit by listing it anyway. While this has no functional benefit its a reminder that this property will also be returned
I have the following in an MVC4 app. I want to filter the load by both keys, there could be many to many but both the keys together represent a unique row. The goal to to explicitly load these collections only when needed and filter by both sides of the relationship.
I have the following Entity.DBContext, it works but only for the UserId key.
context.UserProfiles.Include(o => o.CoachOfOrginizations).Where(p => p.UserId == UserId).Single()
Also this loads all of them, but of course doesnt filter at all, just showing you so you know that I set up my fluent code properly.
context.Entry(this).Collection(t => t.AdministratorOfOrginizations).Load();
modelBuilder.Entity<UserProfile>()
.HasMany(p => p.CoachOfOrginizations)
.WithMany(t => t.Coaches)
.Map(mc =>
{
mc.ToTable("OrganizationCoaches");
mc.MapLeftKey("UserProfileID");
mc.MapRightKey("OrganizationID");
});
CoachOfOrginizations is the following property on UserProfile.
public ICollection<Organization> CoachOfOrginizations { get; set; }
What i would like is to filter my original .Include above based on Organization.ID (the other key) and the currently used key p.UserId. I've tried the following and it doesn't work, any advice? "this" is the UserProfile object.
context.Entry(this)
.Collection(b => b.CoachOfOrginizations)
.Query()
.Where(p => p.ID == orgID)
.Load();
You can do both filtering in a single query. See conditional includes.
var query = context.UserProfiles
.Where(p => p.UserId == UserId)
.Select(p => new { p, Orgs = p.CoachOfOrginizations.Where(o => o.ID == orgID) });
var profiles = query.AsEnumerable().Select(a => a.p);
Say I have:
ProductA
ProductB
ProductScreen
ProductAScreen1 : ProductScreen
ProductAScreen2 : ProductScreen
ProductBScreen1 : ProductScreen
ProductBScreen2 : ProductScreen
How can I set it up so that I register the screens locally to the product? So when I am in ProductA and passing IEnumerable it doesn't resolve ProductB's screens?
I thought this would be achievable using something like the lifetime scope, but it doesn't appear I understood it correctly.
Lifetime scope is used to control instance lifetime. You are talking about controlling selection. For that, you should look at the metadata features in Autofac.
Using metadata, you can "tag" each product screen, indicating which product it belongs to:
builder.Register(c => new ProductAScreen1()).As<ProductScreen>()
.WithMetadata<IProductScreenMetadata>(m =>
m.For(am => am.ProductType, typeof(ProductA)));
builder.Register(c => new ProductAScreen2()).As<ProductScreen>()
.WithMetadata<IProductScreenMetadata>(m =>
m.For(am => am.ProductType, typeof(ProductA)));
builder.Register(c => new ProductBScreen1()).As<ProductScreen>()
.WithMetadata<IProductScreenMetadata>(m =>
m.For(am => am.ProductType, typeof(ProductB)));
builder.Register(c => new ProductBScreen2()).As<ProductScreen>()
.WithMetadata<IProductScreenMetadata>(m =>
m.For(am => am.ProductType, typeof(ProductB)));
Next, you can take a dependency on a IEnumerable<Lazy<ProductScreen, IProductScreenMetadata>> and resolve screens according to product type:
var productScreens = _screens.WHere(a => a.Metadata.ProductType == typeof(ProductA));
Update: for completeness, here is a simpler solution using the Keyed approach. First, registration is much simpler:
builder.RegisterType<ProductAScreen1>().Keyed<ProductScreen>(typeof(ProductA));
builder.RegisterType<ProductAScreen2>().Keyed<ProductScreen>(typeof(ProductA));
builder.RegisterType<ProductBScreen1>().Keyed<ProductScreen>(typeof(ProductB));
builder.RegisterType<ProductBScreen2>().Keyed<ProductScreen>(typeof(ProductB));
To resolve a collection of keyed services we'll have to take a dependency on the IIndex<,> type:
public class SomeService
{
private IEnumerable<ProductScreen>> _screens;
public SomeService(IIndex<Type, IEnumerable<ProductScreen>> screens)
{
_screens = screens;
}
public void DoSomething()
{
var screensForProductA = _screens[typeof(ProductA)];
}
}
Note: for the curious: instead of hardcoding the type registrations, here's how you can do a "by convention" registration:
var assembly = ...;
var productTypes = ...; // a collection of all the product types
foreach(var productType in productTypes)
{
builder.RegisterAssemblyTypes(assembly)
.Where(t => typeof(ProductScreen).IsAssignableFrom(t))
.Where(t => t.Name.StartsWith(productType.Name))
.Keyed<ProductScreen>(productType);
}