I've created a pure join table out of the folowing two tables:
User:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserID { get; set; }
public string UserName { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
public virtual ICollection<CrRole> Roles { get; set; }
Role:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RoleID { get; set; }
public String RoleName { get; set; }
public virtual ICollection<CrUser> Users { get; set; }
Fluent API to create the UserRole join Table:
modelBuilder.Entity<CrUser>()
.HasMany(c => c.Roles)
.WithMany(i => i.Users)
.Map(t => t.MapLeftKey("UserID")
.MapRightKey("RoleID")
.ToTable("CrUserRole"));
This works fine, and creates the correct CrUserRole table with UserID and RoleID columns.
I am having great difficulty in trying to work out how to do a simple query against this join table though. In SQL the command would be:
SELECT COUNT(*)
FROM UserRole ur
WHERE ur.RoleID = #RoleID
AND ur.UserID = #UserID
I've tried going through the navigation links of either the Role or User entities, but always seem to end up with a load of spaghetti code which refuses to compile :-(
I'd be very grateful if someone could point me in the right direction. Even some tutorials would help, the ones I've found only seem to only go as far as creating the join table and not include CRUD operations.
Thanks!
notice your mapping:
t.MapLeftKey("UserID")
.MapRightKey("RoleID")
.ToTable("CrUserRole"));
this mapping shows that your query's result will always be 1 or 0.
The proper way to do this IMO is as follows:
You have both user id and role id right?
First get the user, then query that specific user's roles. Something like:
var user = ctx.Users.FirstOrDefault(u => u.UserID == myUserId);
var role = user.Roles.FirstOrDefault(r => r.RoleId = myRoleId);
Thanks Kaymar.
The version I eventually used was:
String[] returnUsers = new String[1];
var role = context.Roles.First(r=>r.RoleName.Equals(roleName));
returnUsers = role.Users.Select(s => s.UserName).ToArray();
Thanks for getting me in the right direction, and apologies for taking months to update this!
Related
I have three tables:
UserProfiles (UserId is the primary key
UserId Email
------ ------------
9032 bill#any.com
WebpagesRoles (RoleId is the primary key)
RoleId RoleName
------ ------------------
0 Site User
56 Admin
(Note the zero RoleId - that's the tricky bit.)
And WebpagesUsersInRoles, where both columns are foreign keys to the other tables.
I want to add these rows to WebpagesUsersInRoles
UserId RoleId
------ ------
9032 0
9032 56
My C#, EFCore 3.1 application has been deoptimised to aid investigation
foreach (var uirToAdd in uirsToAdd)
{
_context.WebpagesUsersInRoles.Add(uirToAdd);
// var role = _context.WebpagesRoles.FirstOrDefault(r => r.RoleId == uirToAdd.RoleId);
// uirToAdd.Role = role;
await _context.SaveChangesAsync(CancellationToken.None);
}
The code snippet above can write the "Admin" row where RoleId is non-zero, but it can't write the "SiteUser" row where RoleId is zero. It fails on the SaveChangesAsync with the message 'The INSERT statement conflicted with the FOREIGN KEY constraint "fk_RoleId"'.
However, if I uncomment those two lines, it works fine.
Why?
Note, we are not using WebSecurity, just the tables, and so a command like roles.AddUserToRole("admin1", "Admin") is not appropriate. In any case, I'm just as curious about what's causing the problem as in finding a solution.
Edit
For reference, here's the actual model, including some parts I left out of the question for brevity:
public partial class WebpagesRole
{
public WebpagesRole()
{
ActionDropdownRoles = new HashSet<ActionDropdownRole>();
ActionTypeRoles = new HashSet<ActionTypeRole>();
ClientTypeRoles = new HashSet<ClientTypeRole>();
ScimUserGroupRoles = new HashSet<ScimUserGroupRole>();
WebpagesUsersInRoles = new HashSet<WebpagesUsersInRole>();
}
public int RoleId { get; set; }
public string RoleName { get; set; }
public string Description { get; set; }
public virtual ICollection<ActionDropdownRole> ActionDropdownRoles { get; set; }
public virtual ICollection<ActionTypeRole> ActionTypeRoles { get; set; }
public virtual ICollection<ClientTypeRole> ClientTypeRoles { get; set; }
public virtual ICollection<ScimUserGroupRole> ScimUserGroupRoles { get; set; }
public virtual ICollection<WebpagesUsersInRole> WebpagesUsersInRoles { get; set; }
}
Trying to construct a LINQ query that performs a simple inner join, groups the data and sums two of the columns. From the examples I've seen it looks fairly straightforward but I must have missed something along the way.
public class Employee
{
public int Id { get; set; }
public int Name { get; set; }
}
public class Inventory
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public decimal OffSite { get; set; }
public decimal OnSite { get; set; }
}
public class InventoryTotal
{
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
public decimal EmployeeOffSite { get; set; }
public decimal EmployeeOnSite { get; set; }
}
The query I have created looks like this
var result = from a in db.Inventory
join b in db.Employee on a.EmployeeId equals b.Id
group new { a, b } by a.EmployeeId into c
select new InventoryTotal
{
EmployeeId = c.Key,
EmployeeName = c.Name,
EmployeeOffSite = c.Sum(d => d.a.OffSite),
EmployeeOnSite = c.Sum(d => d.a.OnSite)
};
One issue appears to be with the Name column, the only value I want to obtain from the join with Employee. I would like to understand how to properly access that column and better understand how to construct this query as a whole.
EmployeeName = c.Name is not valid, nor are a few other combination I've tried.
So you have two tables: Employees and Inventories. There is a one-to-many relation between these two: Every Employee has zero or more Inventories; every Inventory is the Inventory of exactly one Employee, namely the Employee that the foreign key EmployeeId refers to.
Requirement: from every Employee get his Id and Name, and the total of all his OffSite and OnSite inventories.
Since you are using entity framework, there are three methods to do this. One is to do the (Group-)Join yourself, the other is to let entity framework do the (Group-)Join, and finally, the most intuitive part is to use the virtual ICollection<Inventory.
Do the GroupJoin yourself
Whenever you have a one-to-many relation, like Schools with their Students, Customers with their Orders, or Employees with their Inventories, and you want to start at the "one" side, consider to use one of the overloads of Queryable.GroupJoin.
On the other hand, if you want to start on the "Many" side, if you want the Student with the School he attends, the Order with the Customer who placed the order, consider to use Queryable.Join
You want to fetch "Employees with (some information about) their Inventories, so we'll use a GroupJoin. I'll use the overload of GroupJoin with a parameter resultSelector, so we can specify what we want as result.
var inventoryTotals = dbContext.Employees.GroupJoin(dbContext.Inventories,
employee => employee.Id, // from every Employee take the primary key
inventory => inventory.EmployeeId, // from every Inventory take the foreign key
// parameter resultSelector: from every Employee, and all Inventories that have a foreign
// key that refers to this Employee, make one new
(employee, inventoriesOfThisEmployee) => new InventoryTotal
{
EmployeeId = employee.Id,
EmployeeName = employee.Name,
EmployeeOffSite = inventoriesOfThisEmployee
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = inventoriesOfThisEmployee
.Select(inventory => inventory.OnSite).Sum(),
});
Let Entity Framework do the GroupJoin
This one feels a bit more natural, for every Employee we Select one InventoryTotal, as requested.
var inventoryTotals = dbContext.Employees.Select(employee => new InventoryTotal
{
// Select the Employee properties that you want.
EmployeeId = employee.Id,
EmployeeName = employee.Name,
// Get the inventories of this Employee:
EmployeeOffSite = dbContext.Inventories
.Where(inventory => inventory.EmployeeId == employee.Id)
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = dbContext.Inventories
.Where(inventory => inventory.EmployeeId == employee.Id)
.Select(inventory => inventory.OnSite).Sum(),
});
Use the virtual ICollections
This one feels the most natural. It is also very easy to unit test your usage without a real database.
If you've followed the entity framework conventions, you will have classes similar to:
public class Employee
{
public int Id { get; set; }
public int Name { get; set; }
... // other properties
// Every Employee has zero or more Inventories (one-to-many)
public ICollection<Inventory> Inventories {get; set;}
}
public class Inventory
{
public int Id { get; set; }
public decimal OffSite { get; set; }
public decimal OnSite { get; set; }
... // other properties
// Every Inventory is the Inventory of exactly one Employee, using foreign key
public int EmployeeId { get; set; }
public virtual Employee Employee {get; set;}
}
This is enough for entity framework to detect the tables, the columns of the tables and the relations with the tables (one-to-many, many-to-many, ...). Only if you want to deviate from the conventions: different identifiers for tables and columns, non-default column types etc Attributes or fluent API is needed.
In Entity framework the columns of the tables are represented by the non-virtual properties. The virtual properties represent the relations between the tables.
The foreign key is a column in the table, hence it is non-virtual. The Inventory has no Employee column, hence property Employee is virtual.
Once you've defined the virtual ICollection, the query is simple:
Requirement: from every Employee get his Id and Name, and the total of all his OffSite and OnSite inventories.
var inventoryTotals = dbContext.Employees.Select(employee => new InventoryTotal
{
// Select the Employee properties that you want.
EmployeeId = employee.Id,
EmployeeName = employee.Name,
EmployeeOffSite = employee.Inventories
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = employee.Inventories
.Select(inventory => inventory.OnSite).Sum(),
});
Simple comme bonjour!
You have to add Name to grouping key:
var result = from a in db.Inventory
join b in db.Employee on a.EmployeeId equals b.Id
group a by new { a.EmployeeId, a.Name } into c
select new InventoryTotal
{
EmployeeId = c.Key.EmployeeId,
EmployeeName = c.Key.Name,
EmployeeOffSite = c.Sum(d => d.OffSite),
EmployeeOnSite = c.Sum(d => d.OnSite)
};
I have an odd situation with a many to many relationship and inserting data.
Table 1 (Region):
public class Region
{
public Region()
{
SessionList = new List<Session>();
UserProfileList = new List<UserProfile>();
LeagueEventList = new List<LeagueEvent>();
}
public int RegionId { get; set; }
public string RegionName { get; set; }
public virtual ICollection<Session> SessionList { get; set; }
public virtual ICollection<UserProfile> UserProfileList { get; set; }
public virtual ICollection<LeagueEvent> LeagueEventList { get; set; }
}
Config file for Region Table:
public class RegionConfig : EntityTypeConfiguration<Region>
{
public RegionConfig()
{
HasKey<int>(r => r.RegionId);
Property(r => r.RegionId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
And for Table 2(LeagueEvent) we have the following:
public class LeagueEvent
{
public LeagueEvent()
{
LeagueEventDetails = new List<LeagueEventDetail>();
RegionIds = new List<Region>();
LeagueEventRegistrations = new List<LeagueEventRegistration>();
}
public int EventId { get; set; }
public int VenueId { get; set; }
public virtual Venue venue { get; set; }
public virtual ICollection<LeagueEventDetail> LeagueEventDetails { get; set; }
public virtual ICollection<Region> RegionIds { get; set; }
public virtual ICollection<LeagueEventRegistration> LeagueEventRegistrations { get; set; }
}
With the following config file:
public class LeagueEventConfig : EntityTypeConfiguration<LeagueEvent>
{
public LeagueEventConfig()
{
HasKey<int>(e => e.EventId);
Property(e => e.EventId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(u => u.venue).WithMany(w => w.LeagueEventList).HasForeignKey(f=> f.VenueId).WillCascadeOnDelete(false);
HasMany<Region>(r => r.RegionIds).WithMany(e => e.LeagueEventList).Map(ler =>
{
ler.MapLeftKey("EventId");
ler.MapRightKey("RegionId");
ler.ToTable("EventRegion");
});
}
}
What is happening is upon attempted insert of a League Event which can span multiple regions which is why I have RegionIds the entity is showing the correct RegionId value(s) prior to insert and save. However, after inserted and upon inspecting the tables it shows:
New Record created correctly in LeagueEvent
New Record(s) created in EventRegion with the correct EventId(s) but with new RegionIds
New Records created in the Region table with the RegionName value duplicated.
Unless I have misunderstood there never should be a new record created in Region from a League Event insert. Am I misunderstanding how fluent handles join tables?
UPDATE
I removed the .map command and tested it that way. EF went ahead and created the table but the same problem kept occurring.
So now I manually created the EventRegion table and updated Events and Regions ICollections to point to that table. Now it is inserting records with the correct values from the Regions table but a 0 value from the Events table. It also IS NOT inserting records into the Region table like before.
What seems to be happening is the EventRegion records from the collection are being inserted before the actual event hence the 0 value for EventId in EventRegion.
Odd as it is I would have thought that would generate an error. If you need any code snippets for the update let me know.
Any help would be appreciated.
Thanks,
Chris
Since I never was able to find a solution. I removed the .map statement and then manually created the join table. Everything is working as intended now. Just would have been nice to figure out the issue.
I am trying to do the following with Dapper (and failing).
My POCOs (all code simplified) are:
public class Company
{
public int CompanyId { get; private set; }
public string CompanyName { get; private set; }
public Person CompanyAddress { get; private set; }
public Person Administrator { get; private set; }
}
public class Person
{
public int PersonId { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
}
In the database the Company table has a FK for CompanyAddress and Administrator which maps to the PersonID PK in the Person table. Based on this and this I think the follwoing is how I want to do this:
public static Company Select(IDbConnection connection, int id)
{
Trap.trap();
return connection.Query<Company, Person, Person, Company>("select * from Company left join Person address on Company.CompanyAddress = address.PersonId left join Person admin on Company.Administrator = admin.PersonId where Company.CompanyId = #Id",
(cmpy, addr, admin) => new { PersonId = id }).FirstOrDefault();
}
But that gives me a compile error on the "new { PersonId = id }". What am I getting wrong?
You need to provide the SplitOn parameter to specify where the next table/class starts. You also shouldn't create an anonymous type but use a new scope to initialize the Administrator property of Company:
string sql = #"select c.CompanyId,c.CompanyName, c.CompanyAddress,
address.PersonId, etc. ....
from Company c
left join Person address
on Company.CompanyAddress = address.PersonId
left join Person admin
on Company.Administrator = admin.PersonId
where Company.CompanyId = #Id";
string splitOn = "PersonId"; // maybe two parameters separated by comma, see comment below the answer
return connection.Query<Company, Person, Person, Company>(sql,
(Company cmpy, Person addr, Person admin) => { cmpy.Administrator = admin; return cmpy; }
,null,null,true,splitOn)
.FirstOrDefault();
However, i'm not sure if that works already since you have two joins to the same table. So i think you need an alias for all of the duplicate columns like PersonId. But this migt be helpful anyway.
I'm trying to use the foreign key association approach to achieve a one-to-one association in EF. In my case there's an association between a User and a Team, and I need a navigation property in each of them.
I bumped into a problem when trying to save data.
This is what the models look like:
public class Team
{
public int ID { get; set; }
public string Name { get; set; }
public int OwnerID { get; set; }
public virtual User Owner { get; set; }
}
public class User
{
public int ID { get; set; }
public string UserName { get; set; }
public int TeamID { get; set; }
public virtual Team Team { get; set; }
}
I added these bits to the DbContext OnModelCreating(), as instructed in the blog post referenced above:
modelBuilder.Entity<User>()
.HasRequired(u => u.Team)
.WithMany()
.HasForeignKey(u => u.TeamID);
modelBuilder.Entity<Team>()
.HasRequired(t => t.Owner)
.WithMany()
.HasForeignKey(t => t.OwnerID)
.WillCascadeOnDelete(false);
And now when adding data like this:
User u = new User();
u.UserName = "farinha";
Team t = new Team("Flour Power");
u.Team = t;
t.Owner = u;
context.Users.Add(u);
context.Teams.Add(t);
context.SaveChanges();
or even like this:
User u = new User();
u.UserName = "farinha";
u.Team = new Team("Flour Power");
context.Users.Add(u);
context.SaveChanges();
I'm getting the following error:
Unable to determine a valid ordering
for dependent operations. Dependencies
may exist due to foreign key
constraints, model requirements, or
store-generated values.
Any idea of how to solve this? Am I saving the data in a wrong way?
Thanks in advance
You are not saving data wrong way but it simply cannot work because you defined bidirectional dependency. Team can be saved only if related to already saved user and user can be saved only if related to existing team. You must make one relation optional by marking foreign key property nullable (for example TeamId in User):
public class User
{
public int ID { get; set; }
public string UserName { get; set; }
public int? TeamID { get; set; }
public virtual Team Team { get; set; }
}
Then you must save the User entity first and then you can save Team.
User u = new User();
u.UserName = "farinha";
context.Users.Add(u);
context.SaveChanges();
u.Team = new Team { Name = "Flour Power", OwnerID = u.ID };
context.SaveChanges();