Related
I have a query that grabs some filtered data, but it's giving me some strange results. See the attached image with the VS Code debugger (the var sourceis a Queryable, something like _dbContext.ModelName)
var count= await source.CountAsync();
is giving a different result than
var count2 = (await source.ToListAsync()).Count();
How is this even possible? With these results, everything I thought I knew about EF becomes a lie.
The same is true for the sync methods.
Can anyone explain to me in which scenario is this possible? Could it be a bug in EF Core 3.1?
Context of the program: side project, DataBase is not accessed by anyone, just by me. There is no other operations in this scenario
edit : the variable source has an Include, so it is _dbContext.ModelName.Include(b=>b.OtherModel). When I remove the Include, it works.
edit2 The ModelName.OtherModel property is null in some cases, but OtherModel.Id (the primary key) cannot be null, so, I guess, when Include performs the Join, excludes the occurrences of ModelName that haven't an OtherModel. Could be this?
Under normal circumstances, with referential integrity intact, this cannot happen.
Take a look at the following code, where both count operations will correctly return a result of 3:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public int IceCreamBrandId { get; set; }
public IceCreamBrand Brand { get; set; }
}
public class IceCreamBrand
{
public int IceCreamBrandId { get; set; }
public string Name { get; set; }
public virtual ICollection<IceCream> IceCreams { get; set; } = new HashSet<IceCream>();
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
public DbSet<IceCreamBrand> IceCreamBrands { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=;database=So63071963",
b => b.ServerVersion("8.0.20-mysql"))
//.UseSqlServer(#"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63071963")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>()
.HasData(
new IceCream {IceCreamId = 1, Name = "Vanilla", IceCreamBrandId = 1},
new IceCream {IceCreamId = 2, Name = "Chocolate", IceCreamBrandId = 2},
new IceCream {IceCreamId = 3, Name = "Matcha", IceCreamBrandId = 3});
modelBuilder.Entity<IceCreamBrand>()
.HasData(
new IceCreamBrand {IceCreamBrandId = 1, Name = "My Brand"},
new IceCreamBrand {IceCreamBrandId = 2, Name = "Your Brand"},
new IceCreamBrand {IceCreamBrandId = 3, Name = "Our Brand"});
}
}
internal static class Program
{
private static void Main()
{
//
// Operations with referential integrity intact:
//
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// Does not use INNER JOIN. Directly uses COUNT(*) on `IceCreams`:
// SELECT COUNT(*)
// FROM `IceCreams` AS `i`
var databaseSideCount = context.IceCreams
.Include(s => s.Brand)
.Count();
// Does use INNER JOIN. Counts using Linq:
// SELECT `i`.`IceCreamId`, `i`.`IceCreamBrandId`, `i`.`Name`, `i0`.`IceCreamBrandId`, `i0`.`Name`
// FROM `IceCreams` AS `i`
// INNER JOIN `IceCreamBrands` AS `i0` ON `i`.`IceCreamBrandId` = `i0`.`IceCreamBrandId`
var clientSideCount = context.IceCreams
.Include(s => s.Brand)
.AsEnumerable() // or ToList() etc.
.Count();
Debug.Assert(databaseSideCount == 3);
Debug.Assert(clientSideCount == 3);
Debug.Assert(databaseSideCount == clientSideCount);
}
}
}
Here it is also not possible to damage the referential integrity, because it is guarded by a foreign key constraint in the database.
If you create your database on your own however (using a custom crafted SQL script) and leave out the foreign key constraint, but still let EF Core believe that there is one in place, and then violate the referential integrity by using a non existing ID in a foreign key column, you can get different results for database-side (here 3) and client-side (here 2) count operations:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public int IceCreamBrandId { get; set; }
public IceCreamBrand Brand { get; set; }
}
public class IceCreamBrand
{
public int IceCreamBrandId { get; set; }
public string Name { get; set; }
public virtual ICollection<IceCream> IceCreams { get; set; } = new HashSet<IceCream>();
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
public DbSet<IceCreamBrand> IceCreamBrands { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=;database=So63071963",
b => b.ServerVersion("8.0.20-mysql"))
//.UseSqlServer(#"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63071963")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
}
internal static class Program
{
private static void Main()
{
//
// Operations with referential integrity violated:
//
using var context = new Context();
// Manually create MySQL database with a missing reference between
// the Matcha ice cream and any brand.
context.Database.ExecuteSqlRaw(
#"
DROP DATABASE IF EXISTS `So63071963`;
CREATE DATABASE `So63071963`;
USE `So63071963`;
CREATE TABLE `IceCreamBrands` (
`IceCreamBrandId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
CONSTRAINT `PK_IceCreamBrands` PRIMARY KEY (`IceCreamBrandId`)
);
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`IceCreamBrandId` int NOT NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
);
INSERT INTO `IceCreamBrands` (`IceCreamBrandId`, `Name`) VALUES (1, 'My Brand');
INSERT INTO `IceCreamBrands` (`IceCreamBrandId`, `Name`) VALUES (2, 'Your Brand');
INSERT INTO `IceCreams` (`IceCreamId`, `IceCreamBrandId`, `Name`) VALUES (1, 1, 'Vanilla');
INSERT INTO `IceCreams` (`IceCreamId`, `IceCreamBrandId`, `Name`) VALUES (2, 2, 'Chocolate');
/* Use non-existing brand id 0: */
INSERT INTO `IceCreams` (`IceCreamId`, `IceCreamBrandId`, `Name`) VALUES (3, 0, 'Matcha');
");
// Does not use INNER JOIN. Directly uses COUNT(*) on `IceCreams`:
// SELECT COUNT(*)
// FROM `IceCreams` AS `i`
var databaseSideCount = context.IceCreams
.Include(s => s.Brand)
.Count();
// Does use INNER JOIN. Counts using Linq:
// SELECT `i`.`IceCreamId`, `i`.`IceCreamBrandId`, `i`.`Name`, `i0`.`IceCreamBrandId`, `i0`.`Name`
// FROM `IceCreams` AS `i`
// INNER JOIN `IceCreamBrands` AS `i0` ON `i`.`IceCreamBrandId` = `i0`.`IceCreamBrandId`
var clientSideCount = context.IceCreams
.Include(s => s.Brand)
.AsEnumerable() // or ToList() etc.
.Count();
Debug.Assert(databaseSideCount == 3);
Debug.Assert(clientSideCount == 2);
Debug.Assert(databaseSideCount != clientSideCount);
}
}
}
We are porting our existing MVC6 EF6 application to Core.
Is there a simple method in EF Core to update a many-to-many relationship?
My old code from EF6 where we clear the list and overwrite it with the new data no longer works.
var model = await _db.Products.FindAsync(vm.Product.ProductId);
model.Colors.Clear();
model.Colors = _db.Colors.Where(x =>
vm.ColorsSelected.Contains(x.ColorId)).ToList();
This will work for you.
Make a class to have the relationship in:
public class ColorProduct
{
public int ProductId { get; set; }
public int ColorId { get; set; }
public Color Color { get; set; }
public Product Product { get; set; }
}
Add a ColorProduct collection to your Product and Color classes:
public ICollection<ColorProduct> ColorProducts { get; set; }
Then use this extension I made to remove the unselected and add the newly selected to the list:
public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
{
db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
}
public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
{
return items
.GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
.SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
.Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
.Select(t => t.t.item);
}
Using it looks like this:
var model = _db.Products
.Include(x => x.ColorProducts)
.FirstOrDefault(x => x.ProductId == vm.Product.ProductId);
_db.TryUpdateManyToMany(model.ColorProducts, vm.ColorsSelected
.Select(x => new ColorProduct
{
ColorId = x,
ProductId = vm.Product.ProductId
}), x => x.ColorId);
In order to avoid the LINQ hell in the above answer, the templated "Except" method can be rewritten as such:
public static IEnumerable<TEntity> LeftComplementRight<TEntity, TKey>(
this IEnumerable<TEntity> left,
IEnumerable<TEntity> right,
Func<TEntity, TKey> keyRetrievalFunction)
{
var leftSet = left.ToList();
var rightSet = right.ToList();
var leftSetKeys = leftSet.Select(keyRetrievalFunction);
var rightSetKeys = rightSet.Select(keyRetrievalFunction);
var deltaKeys = leftSetKeys.Except(rightSetKeys);
var leftComplementRightSet = leftSet.Where(i => deltaKeys.Contains(keyRetrievalFunction.Invoke(i)));
return leftComplementRightSet;
}
Furthermore the UpdateManyToMany method can be updated to include entities that have been modified as such:
public void UpdateManyToMany<TDependentEntity, TKey>(
IEnumerable<TDependentEntity> dbEntries,
IEnumerable<TDependentEntity> updatedEntries,
Func<TDependentEntity, TKey> keyRetrievalFunction)
where TDependentEntity : class
{
var oldItems = dbEntries.ToList();
var newItems = updatedEntries.ToList();
var toBeRemoved = oldItems.LeftComplementRight(newItems, keyRetrievalFunction);
var toBeAdded = newItems.LeftComplementRight(oldItems, keyRetrievalFunction);
var toBeUpdated = oldItems.Intersect(newItems, keyRetrievalFunction);
this.Context.Set<TDependentEntity>().RemoveRange(toBeRemoved);
this.Context.Set<TDependentEntity>().AddRange(toBeAdded);
foreach (var entity in toBeUpdated)
{
var changed = newItems.Single(i => keyRetrievalFunction.Invoke(i).Equals(keyRetrievalFunction.Invoke(entity)));
this.Context.Entry(entity).CurrentValues.SetValues(changed);
}
}
which uses another custom templated "Intersect" function to find the intersection of the two sets:
public static IEnumerable<TEntity> Intersect<TEntity, TKey>(
this IEnumerable<TEntity> left,
IEnumerable<TEntity> right,
Func<TEntity, TKey> keyRetrievalFunction)
{
var leftSet = left.ToList();
var rightSet = right.ToList();
var leftSetKeys = leftSet.Select(keyRetrievalFunction);
var rightSetKeys = rightSet.Select(keyRetrievalFunction);
var intersectKeys = leftSetKeys.Intersect(rightSetKeys);
var intersectionEntities = leftSet.Where(i => intersectKeys.Contains(keyRetrievalFunction.Invoke(i)));
return intersectionEntities;
}
I have a A Table, B Table and AB (Mapping Table)
A
public class A
{
public int AID{ get; set; }
[JsonIgnore]
public virtual ICollection<B> Bs { get; set; }
}
B
public class B
{
public int BID { get; set; }
[JsonIgnore]
public virtual ICollection<A> As { get; set; }
}
ApplicationDbContext
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<B>()
.HasMany(s => s.As)
.WithMany(c => c.Bs)
.Map(cs =>
{
cs.MapLeftKey("AID");
cs.MapRightKey("BID");
cs.ToTable("AB");
});
}
Now things are perfectly fine, but how do I insert in this AB Mapping table?
If I try to create AB as like below, it generates two tables, AB and AB1 with same column name and all.
public class AB
{
public int ABID { get; set; }
public string AID { get; set; }
public int BID { get; set; }
}
So is there any way to do CRUD in FluentAPI Mapping Table?
If not, then can I force FluentAPI to map from Existing table? In this case I'll manually manage Employee and will change the mapping code to use existing table.
I'm unable to find any of the solution.
Edit: Since the question was changed, I'm writing up a more thorough answer. The answer to your question remains the same, however:
Now things are perfectly fine, but how do I insert in this AB Mapping
table?
You don't!
This is exactly the kind of thing that EF is good at. Instead of managing a link table yourself, now you just end up with the actual object you want. So, if you want to add a link between an A and B, all you do is add a B to the Bs collection on that A. You don't ever insert directly into the AB table, because who cares about that? That table is there so we can have relationships between different As and Bs, that's it. So, Entity Framework will create the table for it's own use, but not present it to you, because that's not how EF works: you work with your objects and let EF handle the database.
That's why when you try to define the table yourself, it creates two: it's already making a table called AB, but you're asking for another one. It can't have exactly the same name so it appends a '1' to the end of it. Since you've already used FluentAPI to define the apping, let EF worry about how to implement the mapping: all you need to care about is that you've now got a way to have an A with a set of Bs, or vice versa.
Since this still sounds confusing with names 'A' and 'B', below is the Program class for a console app that will illustrate this; all you need to do is start a fresh console app, replace the Program class with this one, install the entity framework package, and run enable-migrations -enableautomaticmigrations -force. I recommend you use this to add some objects and relate them, and then go have a look at your database: you will see the 'AB' table, with records that were added. This might help explain it better.
class Program
{
static bool quit = false;
static void Main(string[] args)
{
string s = "Please select an option:" +
"\n1: Insert an A" +
"\n2: Insert a B" +
"\n3: Add a B to an A" +
"\n4: Add an A to a B" +
"\n5: Print all As" +
"\n6: Print all Bs" +
"\n7: Print AB Table" +
"\nx: Quit.";
while (!quit)
{
Console.WriteLine();
Console.WriteLine(s);
var k = Console.ReadKey();
DoStuff(k);
}
}
private static void DoStuff(ConsoleKeyInfo i)
{
switch (i.Key)
{
case ConsoleKey.D1:
//add an A
AddA(GetName());
break;
case ConsoleKey.D2:
//add a B
AddB(GetName());
break;
case ConsoleKey.D3:
// link a B to an A
LinkB(GetBtoLink(),GetAtoLink());
break;
case ConsoleKey.D4:
//link an A to an B
LinkA(GetAtoLink(), GetBtoLink());
break;
case ConsoleKey.D5:
// print As
WriteA();
break;
case ConsoleKey.D6:
//print Bs
WriteB();
break;
case ConsoleKey.D7:
// print AB
WriteAB();
break;
case ConsoleKey.X:
quit = true;
break;
}
}
private static int GetAtoLink()
{
string x;
int z;
do
{
Console.Clear();
Console.WriteLine("Please enter the ID of the A you want to use and then press enter.");
WriteA();
x = Console.ReadLine();
} while (!int.TryParse(x, out z));
return z;
}
private static int GetBtoLink()
{
string x;
int z;
do
{
Console.Clear();
Console.WriteLine("Please enter the ID of the B you want to use and then press enter.");
WriteB();
x = Console.ReadLine();
} while (!int.TryParse(x, out z));
return z;
}
private static void WriteB()
{
Console.WriteLine("{0,10}{1,15}", "ID", "Name");
using (var db = new Context())
{
foreach (var a in db.Bs)
{
Console.WriteLine("{0,10}{1,15}", a.BID, a.Name);
}
}
}
private static void WriteA()
{
Console.WriteLine("{0,10}{1,15}", "ID", "Name");
using (var db = new Context())
{
foreach (var a in db.As)
{
Console.WriteLine("{0,10}{1,15}", a.AID, a.Name);
}
}
}
private static void WriteAB()
{
Console.WriteLine("{0,10}{1,10}", "AID", "BID");
using (var db = new Context())
{
// this is the only way we need to do this, because it's many to many,
// if an A is linked to a B, then that B is by definition linked to that A as well.
foreach (var a in db.As)
{
foreach (var b in a.Bs)
{
Console.WriteLine("{0,10}{1,10}", a.AID, b.BID);
}
}
}
}
private static void LinkB(int bToUse, int aToUse)
{
using (var db = new Context())
{
var a = db.As.First(x => x.AID == aToUse);
var b = db.Bs.First(y => y.BID == bToUse);
a.Bs.Add(b);
db.SaveChanges();
}
}
private static void LinkA(int aToUse, int bToUse)
{
using (var db = new Context())
{
var a = db.As.First(x => x.AID == aToUse);
var b = db.Bs.First(y => y.BID == bToUse);
b.As.Add(a);
db.SaveChanges();
}
}
private static string GetName()
{
Console.WriteLine("Please enter a name");
return Console.ReadLine();
}
private static void AddA(string input)
{
using (var db = new Context())
{
db.As.Add(new A {Name = input});
db.SaveChanges();
}
}
private static void AddB(string input)
{
using (var db = new Context())
{
db.Bs.Add(new B { Name = input });
db.SaveChanges();
}
}
}
public class A
{
public int AID { get; set; }
public string Name { get; set; }
public virtual ICollection<B> Bs { get; set; }
}
public class B
{
public int BID { get; set; }
public string Name { get; set; }
public virtual ICollection<A> As { get; set; }
}
public class Context : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<B>()
.HasMany(s => s.As)
.WithMany(c => c.Bs)
.Map(cs =>
{
cs.MapLeftKey("AID");
cs.MapRightKey("BID");
cs.ToTable("AB");
});
}
public DbSet<A> As { get; set; }
public DbSet<B> Bs { get; set; }
}
Old Answer: You've defined an ICollection<ApplicationUser> called Employees in Company, and mapped to it with FluentAPI. This creates a table called 'Employees' as expected. You don't have to create another class called Employees; as far as Entity Framework is concerned, you've already told it to create a table called Employees. This is why
I think the step you're missing is defining your DbSet<>.
Using your code, and running Add-Migration, this is the definition I get for the Employees table:
CreateTable(
"dbo.Employees",
c => new
{
UserID = c.Int(nullable: false),
CompanyID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.UserID, t.CompanyID })
.ForeignKey("dbo.ApplicationUsers", t => t.UserID, cascadeDelete: true)
.ForeignKey("dbo.Companies", t => t.CompanyID, cascadeDelete: true)
.Index(t => t.UserID)
.Index(t => t.CompanyID);
Which seems to correlate with what you wanted.
To finish it off, add (if you haven't already) this to your ApplicationDbContext file:
public DbSet<ApplicationUser> Employees;
public DbSet<Company> Companies;
Then to add an employee, you create a new ApplicationUser and add it like
ApplicationUser user = new ApplicationUser();
// do whatever here to give it the right data
ApplicationDbContext ctx = new ApplicationDbContext();
ctx.Employees.Add(user);
The Employees table itself you shouldn't ever have to interact with.
EF will manage that you don't need to insert into the mapping table directly, have a look at this sample that I have in my project:
public class Organization : Entity<int>
{
public string Name { get; set; }
public string Address { get; set; }
public string MainContact { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
//navigation property
public virtual ICollection<DevelopmentalGoal> DevelopmentalGoals { get; set; }
public virtual ICollection<ServiceActivity> ServiceActivities { get; set; }
}
public class DevelopmentalGoal : Entity<int>
{
public string Name { get; set; }
public string Icon { get; set; }
//navigation property
public virtual ICollection<Organization> Organizations { get; set; }
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Organization>().ToTable("Organization", "ServiceLearning")
.HasKey(t => t.ID);
modelBuilder.Entity<DevelopmentalGoal>().ToTable("DevelopmentalGoal", "ServiceLearning")
.HasKey(t => t.ID);
modelBuilder.Entity<Organization>()
.HasMany(t => t.DevelopmentalGoals)
.WithMany(t=> t.Organizations)
.Map(m =>
{
m.ToTable("OrganizationDevelopmentalGoal", "ServiceLearning");
m.MapLeftKey("OrganizationID");
m.MapRightKey("DevelopmentalGoalID");
});
}
public int SaveOrganization(OrganizationViewModel viewModel, IUserContext currentUser)
{
Organization organization;
{
if (viewModel.ID == 0)
{
organization = ObjectMapper.MapTo<Organization>(viewModel);
_context.Set<Organization>().Add(organization);
}
else
{
organization = _context.Set<Organization>()
.SingleOrDefault(t =>
t.ID == viewModel.ID
);
organization.Name = viewModel.Name;
organization.Address = viewModel.Address;
organization.MainContact = viewModel.MainContact;
organization.Phone = viewModel.Phone;
organization.Website = viewModel.Website;
UpdateOrganizationDevelopmentalGoals(organization, viewModel);
}
try
{
CommitChanges();
}
catch (DbUpdateException ex)
{
if (ex.IsDuplicateException())
throw new KeystoneDuplicateException("A Organization with the same name already exists.");
throw ex;
}
}
return organization.ID;
}
private void UpdateOrganizationDevelopmentalGoals(Organization organization, OrganizationViewModel viewModel)
{
var originalIdList = organization.DevelopmentalGoals.Select(d => d.ID).Distinct().ToList();
var modifiedIdList = viewModel.DevelopmentalGoal.Where(d => d.Selected == true).Select(d => d.ID).Distinct().ToList();
//Remove deleted Developmetal Goals.
foreach (var id in originalIdList.Except(modifiedIdList))
organization.DevelopmentalGoals.Remove(organization.DevelopmentalGoals.Single(d => d.ID == id));
//Add new Developmetal Goals.
foreach (var id in modifiedIdList.Except(originalIdList))
{
//Add director relationship without having to load entity.
var d = new DevelopmentalGoal { ID = id };
_context.Set<DevelopmentalGoal>().Attach(d);
organization.DevelopmentalGoals.Add(d);
}
}
As you can see in the UpdateOrganizationDevelopmentalGoals method I do not insert or delete data from the mapping table directly, I insert and delete from the organization.DevelopmentalGoals and as I've already defined the mapping table in fluent API on "OnModelCreating" then EF knows how to manage the relations.
In Entity framework I have objectsets like
public partial class Building
{
public int BuildingID { get; set; }
public string BuildingName { get; set; }
}
public partial class Town
{
public int TownID { get; set; }
public string TownName { get; set; }
}
I want to create a generic query like
T.OrderBy(o=>o.Id).Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
T is generic class can be Building or Town but problem is BuildingId and TownId has different name.I don't want to change their name as Id and create interface IIdentity.
Maybe you could try something like this:
var query = (typeof(T) == typeof(Building) ?
context.Buildings.Select(b => new { Id = b.BuildingId, Name = b.BuildingName }) :
context.Towns.Select(t => new { Id = t.TownId, Name = b.TownName }))
.OrderBy(o => o.Id)...
Not tested but that's worth a test...
You can create generic method which find a field decorated with KeyAttribute, and then performs sorting by found key field. I have tested your model, works perfectly. Look at code snippet.
DbContext:
using System.Collections.Generic;
using System.Data.Entity;
namespace ConsoleApplication28.Entities
{
public class AppDbContext : DbContext
{
public AppDbContext()
{
Database.Connection.ConnectionString = #"Data Source=NOTEBOOK-PC;Initial Catalog=StackOverflowTest;Integrated Security=True";
Database.SetInitializer(new AppDbInitializer());
}
public DbSet<Town> Towns { get; set; }
public DbSet<Building> Buildings { get; set; }
}
public class AppDbInitializer : DropCreateDatabaseIfModelChanges<AppDbContext>
{
protected override void Seed(AppDbContext context)
{
context.Buildings.AddRange(new List<Building>
{
new Building {BuildingName = "Building1"},
new Building {BuildingName = "Building2"},
});
context.Towns.AddRange(new List<Town>
{
new Town {TownName = "Town1"},
new Town {TownName = "Town2"},
});
context.SaveChanges();
base.Seed(context);
}
}
}
Building
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28.Entities
{
public class Building
{
[Key]
public int BuildingID { get; set; }
public string BuildingName { get; set; }
}
}
Town
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28.Entities
{
public class Town
{
[Key]
public int TownID { get; set; }
public string TownName { get; set; }
}
}
Program
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ConsoleApplication28.Entities;
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28
{
class Program
{
static void Main(string[] args)
{
const int maxDispItem = 10;
const int page = 1;
var db = new AppDbContext();
var towns = db.Towns.OrderByKey().Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
var buildings = db.Buildings.OrderByKey().Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
}
}
public static class Extensions
{
/// <summary>
/// Sorts the elements of a sequence in ascending order according to a key specified using KeyAttribute
/// </summary>
public static IOrderedQueryable<T> OrderByKey<T>(this IQueryable<T> source, bool isAsc = true)
{
var type = typeof(T);
var keyProperty = type.GetProperties().Single(x => x.GetCustomAttributes(typeof(KeyAttribute)).Any());
return source.OrderBy(keyProperty.Name, isAsc);
}
#region COPIED FROM THERE http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property, bool isAsc)
{
return isAsc ? source.OrderBy(property) : source.OrderByDescending(property);
}
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderBy");
}
public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { source, lambda });
return (IOrderedQueryable<T>)result;
}
#endregion
}
}
I'm trying to create a datamodel using EF 4.1 and code first.
My Poco classes looks like this:
public abstract class AttributeBase
{
public int Id { get; set; }
public string Name { get; set; }
public string AttributeDescription { get; set; }
public int Length { get; set; }
public bool AllowNull { get; set; }
public bool DisplayInWeb { get; set; }
}
public class StringListAttribute : AttributeBase
{
public ICollection<StringValue> Values { get; set; }
public StringListAttribute()
{
Values = new List<StringValue>();
}
}
public class StringValue
{
public int StringValueId { get; set; }
public string Value { get; set; }
}
The basic idea is that StringListAttribute inherits from the AttributeBase class (other classes also inherits from the AttributeBase class, but I didn't include them in this post)
The StringListAttribute class have a collection of StringValues. (a zero-to-many relationship). What I try to achieve in my MyContextExtention class is that whenever I want a list of AttributeBase objects from the database, I want to get all the StringListAttribute objects and to populate their StringValue collection in one go. I don't want any lazy loading. I want to retrieve as much data I can in as few queries as possible.
My Context class inherits from DbContext and I created an extension method on my BraArkivContext class
public class MyContext : DbContext
{
public DbSet<AttributeBase> Attributes { get; set; }
public MyContext()
{
Configuration.LazyLoadingEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<StringListAttribute>()
.HasMany(t=>t.Values).WithOptional().WillCascadeOnDelete();
base.OnModelCreating(modelBuilder);
}
}
public static class MyContextExtension
{
private const string Values = "Values";
public static List<AttributeBase> AttributesWithData1(this MyContext context)
{
var attributeBases = new List<AttributeBase>();
var stringListAttributes = context.Attributes.Include(Values).OfType<StringListAttribute>().ToList();
attributeBases.AddRange(stringListAttributes);
return attributeBases;
}
}
My test project (I'm using nUnit) )is nothing much to write home about and contains these methods:
public class TestHelper
{
public static StringListAttribute CreateStringListAttribute(string name, int listSize)
{
var listAttribute = new StringListAttribute();
listAttribute.Name = name;
for (int i = 0; i < listSize; i++)
{
var attr1 = new StringValue();
attr1.Value = String.Format("StringValue_{0}", i);
listAttribute.Values.Add(attr1);
}
return listAttribute;
}
}
[Test]
public void AttributeBase_GetAllAttributesWithData1()
{
//First add some data to the database
var numberOfObjects = 100;
var name = "StringListAttribute_ReadMultipleStringsListsFromDb_" + Guid.NewGuid().ToString();
var listAttributes = TestHelper.CreateStringLists(name, numberOfObjects);
using (var context = new MyContext())
{
foreach (var stringListAttribute in listAttributes)
{
context.Attributes.Add(stringListAttribute);
}
context.SaveChanges();
}
Stopwatch sw = new Stopwatch()
var attributes = new List<AttributeBase>();
using (var context = new MyContext())
{
sw.Start();
attributes = context.AttributesWithData1().ToList();
sw.Stop();
}
Debug.WriteLine(String.Format("Total time for {0} attributes is: {1}.", attributes.Count, sw.Elapsed));
Assert.IsNotNull(attributes);
}
When running the test method "AttributeBase_GetAllAttributesWithData1" The sql statement that is executed looks like this:
SELECT
[Project1].[Id] AS [Id],
[Project1].[C1] AS [C1],
[Project1].[Name] AS [Name],
[Project1].[AttributeDescription] AS [AttributeDescription],
[Project1].[Length] AS [Length],
[Project1].[AllowNull] AS [AllowNull],
[Project1].[DisplayInWeb] AS [DisplayInWeb],
[Project1].[Document_Id] AS [Document_Id],
[Project1].[C2] AS [C2],
[Project1].[StringValueId] AS [StringValueId],
[Project1].[Value] AS [Value],
[Project1].[StringListAttribute_Id] AS [StringListAttribute_Id]
FROM ( SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[AttributeDescription] AS [AttributeDescription],
[Extent1].[Length] AS [Length],
[Extent1].[AllowNull] AS [AllowNull],
[Extent1].[DisplayInWeb] AS [DisplayInWeb],
[Extent1].[Document_Id] AS [Document_Id],
'0X0X' AS [C1],
[Extent2].[StringValueId] AS [StringValueId],
[Extent2].[Value] AS [Value],
[Extent2].[StringListAttribute_Id] AS [StringListAttribute_Id],
CASE WHEN ([Extent2].[StringValueId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
FROM [dbo].[AttributeBases] AS [Extent1]
LEFT OUTER JOIN [dbo].[StringValues] AS [Extent2] ON [Extent1].[Id] = [Extent2].[StringListAttribute_Id]
WHERE [Extent1].[Discriminator] = 'StringListAttribute'
) AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C2] ASC
When I run this sql statement in SQL Server Management Studio it's quite fast, but the Entity Framework is taking a very long time to process the result and return the poco classes populated with data.
Is there any other way to model the AttributeBase, StringListAttribute and StringValue classes and the relation between them to decrease the processing time in EF?