So I'm running into an annoying problem with EF Core 6.0 where I am getting a naming conflict when mapping using strongly typed Value Objects as Id's.
So the following, using straight Guid's as Id's, works:
public class User {
public Guid Id { get; set; }
public List<UserRole> Roles { get; set; }
}
public class UserRole {
public Guid UserId { get; set; }
public Guid RoleId { get; set; }
}
internal sealed class UserEntityTypeConfiguration : IEntityTypeConfiguration<User> {
public void Configure(EntityTypeBuilder<User> builder) {
builder.ToTable("User", SchemaNames.Application);
builder.HasKey(p => p.Id).HasName("Id");
builder.HasMany(d => d.Roles);
}
}
internal sealed class UserRoleValueTypeConfiguration : IEntityTypeConfiguration<UserRole> {
public void Configure(EntityTypeBuilder<UserRole> builder) {
builder.ToTable("UserRoles", SchemaNames.Application);
builder.HasKey(userRole => new { userRole.UserId, userRole.RoleId });
builder.Property("UserId").HasColumnName("UserId");
builder.Property("RoleId").HasColumnName("RoleId");
}
}
User user = new User() { Id = Guid.NewGuid() };
user.Roles.Add(new UserRole() { UserId = user.Id.Value, RoleId = roleId });
context.Users.AddAsync(user);
However when trying to use strongly typed Id's and perform an Add:
public class UserId : TypedIdValue {
public UserId(Guid value) : base(value) { }
}
public class User {
public UserId Id { get; set; }
public List<UserRole> Roles { get; set; }
}
User user = new User() { Id = Guid.NewGuid() };
user.Roles.Add(new UserRole() { UserId = user.Id.Value, RoleId = roleId });
context.Users.AddAsync(user);
I get - SqlException: Invalid column name 'UserId1'.
I am using a custom ValueConverterSelector and ValueConverter registered with the DbContextOptionsBuilder to facilitate the strongly typed Id's.
Clearly it's tripping up over there being a UserId object for Id and UserId field in the UserRole object, but I'm at a loss as to how to resolve this issue.
Any help would be greatly appreciated.
Related
I have these classes
public class UserRole
{
public int RoleId { get; set; }
public int UserId { get; set: }
public virtual User User { get; set; }
public virtual Role Role { get; set; }
}
[Table("User")]
public class User
{
public User()
{
UserRoles = new HashSet<UserRole>();
}
public int FirstName { get; set; }
public int UserId { get; set: }
public virtual ICollection<UserRole> UserRoles { get; set; }
}
[Table("Role")]
public class Role
{
public Role()
{
UserRoles = new HashSet<UserRole>();
}
public int RoleId { get; set; }
public string RoleName { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; }
}
I am trying to do simple insert into the UserRole table
public class Run
{
DbContext context;
public Run()
{
context = new DbContext();
}
public void Validate(User user, int roleId)
{
InsertUserRole(user,roleId)
context.SaveChanges();
}
}
public void InsertUserRole(user targetUser, int roleId)
{
UserRole targetUserRole = targetUser.UserRoles
.Where(u => u.RoleId == roleId)
.FirstOrDefault();
if (targetUserRole == null)
{
targetUserRole = new UserRole();
targetUserRole.RoleId = roleId;
targetUserRole.UserId = targetUser.UserId;
context.UserRoles.Add(targetUserRole);
}
}
When I am trying to insert into UserRole table, I get an exception
Violation of Unique KEy 'UQ_Role_Name'.Cannot insert duplicate key in dbo.Role
I need to insert row into UserRole as the role does not exist for user, and the role is present in Role table.
Please let me know how I can insert into Userole table using Entity Framework context
I'm not sure if there is enough info about your entities/schema to narrow down the specific cause of your issue, but a few things do come to mind.
Firstly, how are you nominating the PK for the UserRole entity? This should either be done with attributes in the entity or via entity configuration:
public class UserRole
{
[Key, Column(Order=0), ForeignKey("User")]
public int UserId { get; set; }
[Key, Column(Order=1), ForeignKey("Role")]
public int RoleId { get; set; }
public virtual User User { get; set; }
public virtual Role Role { get; set; }
}
Next, to add a new UserRole it should be possible to do so by setting the IDs, though generally I recommend setting the navigation properties rather than the IDs. The reasons for this is that this validates the values you are passing, and the entity should be considered "complete" (navigation properties are available) after saving.
For your InsertUserRole method, there are a couple of adjustments there. You don't need to load the existing entity, a simple exists check would be sufficient and faster, then loading the relevant references. This can either be inserted to a UserRole DBSet, or added to the requested user:
public void InsertUserRole(int userId, int roleId)
{
var userRoleExists = context.UserRoles.Any(x => x.UserId == userId && x.RoleId == roleId);
if(userRoleExists) return;
var user = context.Users.Single(x => x.UserId == userId);
var role = context.Roles.Single(x => x.RoleId == roleId);
UserRole userRole = new UserRole
{
User = user,
Role = role
};
context.UserRoles.Add(userRole);
context.SaveChanges();
}
or alternatively:
public void InsertUserRole(int userId, int roleId)
{
var user = context.Users
.Include(x => x.UserRoles)
.Single(x => x.UserId == userId);
if(user.UserRoles.Any(x => x.RoleId == roleId) return;
var role = context.Roles.Single(x => x.RoleId == roleId);
UserRole userRole = new UserRole
{
User = user,
Role = role
};
user.UserRoles.Add(userRole);
context.SaveChanges();
}
I am assigned the implementation of a REST GET with a complex DB model and somewhat complex output layout. Although I am a REST beginner, I have lost "rest" on this for 2 weeks spinning my wheels, and Google was of no help as well.
Here's a simplification of the existing DB I am given to work with:
Table group : {
Column id Guid
Column name string
Primary key: {id}
}
Table account
{
Column id Guid
Column name string
Primary key: {id}
}
Table groupGroupMembership
{
Column parentGroupId Guid
Column childGroupId Guid
Primary key: {parentGroupId, childGroupId}
}
Table accountGroupMembership
{
Column parentGroupId Guid
Column childAccountId Guid
Primary key: {parentGroupId, childAccountId}
}
So clearly you guessed it: There is a many-to-many relationship between parent a child groups. Hence a group can have many parent and child groups. Similarly, an account can have many parent groups.
The DB model I came up with in C# (in namespace DBAccess.Models.Tables):
public class Group
{
// properties
public Guid id { get; set; }
public string? name { get; set; }
// navigation properties
public List<GroupMemberAccount>? childAccounts { get; set; }
public List<GroupMemberGroup>? childGroups { get; set; }
public List<GroupMemberGroup>? parentGroups { get; set; }
}
public class Account
{
// properties
public Guid id { get; set; }
public string? name { get; set; }
// navigation properties
public List<GroupMemberAccount>? parentGroups { get; set; }
}
public class GroupMemberAccount
{
// properties
public Guid parentGroupId { get; set; }
public Guid childAccountId { get; set; }
//navigation properties
public Group? parentGroup { get; set; }
public Account? childAccount { get; set; }
static internal void OnModelCreating( EntityTypeBuilder<GroupMemberAccount> modelBuilder )
{
modelBuilder.HasKey(gma => new { gma.parentGroupId, gma.childAccountId });
modelBuilder
.HasOne(gma => gma.parentGroup)
.WithMany(g => g.childAccounts)
.HasForeignKey(gma => gma.parentGroupId);
modelBuilder
.HasOne(gma => gma.childAccount)
.WithMany(a => a.parentGroups)
.HasForeignKey(gma => gma.childAccountId);
}
}
public class GroupMemberGroup
{
// properties
public Guid parentGroupId { get; set; }
public Guid childGroupId { get; set; }
//navigation properties
public Group? parentGroup { get; set; }
public Group? childGroup { get; set; }
static internal void OnModelCreating(EntityTypeBuilder<GroupMemberGroup> modelBuilder)
{
modelBuilder.HasKey(gmg => new { gmg.parentGroupId, gmg.childGroupId });
modelBuilder
.HasOne(gmg => gmg.parentGroup)
.WithMany(g => g.childGroups)
.HasForeignKey(gmg => gmg.parentGroupId);
modelBuilder
.HasOne(gmg => gmg.childGroup)
.WithMany(g => g.parentGroups)
.HasForeignKey(gmg => gmg.childGroupId);
}
}
The corresponding DTO model I created:
public class Account
{
public Guid id { get; set; }
public string? name { get; set; }
public List<GroupMemberAccount>? parentGroups { get; set; }
}
public class AccountMappingProfile : AutoMapper.Profile
{
public AccountMappingProfile()
{
CreateMap<DBAccess.Models.Tables.Account, Account>();
}
}
public class Group
{
public Guid id { get; set; }
public string? Name { get; set; }
public GroupChildren children { get; set; } = null!;
};
public class GroupChildren
{
public List<GroupMemberAccount>? childAccounts { get; set; } = null!;
public List<GroupMemberGroup>? childGroups { get; set; } = null!;
}
public class GroupMemberAccount
{
public Guid parentGroupId { get; set; }
public Guid childAccountId { get; set; }
//public Group? parentgroup { get; set; } // commented out because no need to output in a GET request
public Account? childAccount { get; set; }
}
public class GroupMemberGroup
{
public Guid parentGroupid { get; set; }
public Guid childGroupId { get; set; }
//public Group? parentGroup { get; set; }; // commented out because no need to output in a GET request
public Group? childGroup { get; set; };
}
What you need to spot here is the difference in classes Group between the DB and DTO models.
In the DB model, Group has 3 lists: childAccounts, childGroups and parentGroups.
In the DTO model, Group has 1 node children of type GroupChildren which is a class that contains 2 of those 3 lists.
Hence an additional difficulty when it comes to design the mapping. That difference is intentional because it matches the following desired output for an endpoint such as: GET .../api/rest/group({some group guid}) is something like:
{
"id": "some group guid",
"name": "some group name",
"children": {
"childAccounts":{
"account":{ "name": "some account name 1"}
"account":{ "name": "some account name 2"}
...
}
"childFroups":{
"group":{ "name": "some group name 1"}
"group":{ "name": "some group name 2"}
...
}
},
}
obtained from following typical controller code:
[HttpGet("Groups({key})")]
[ApiConventionMethod(typeof(ApiConventions),
nameof(ApiConventions.GetWithKey))]
public async Task<ActionResult<Group>> Get(Guid key, ODataQueryOptions<Group> options)
{
var g = await (await context.Group.Include(g => g.childAccounts)
.Include(g => g.childGroups)
.Where(g => g.id == key)
.GetQueryAsync(mapper, options) // note the mapper here is the mapping defined below
).FirstOrDefaultAsync();
if (g is null)
{
return ResourceNotFound();
}
return Ok(g);
}
So here's the missing part to all this. Unless there are major errors in all of the above, I have a very strong intuition that it is the mapping that is failing to get me the requested output above.
public class GroupMappingProfile : AutoMapper.Profile
{
public GroupMappingProfile()
{
// the rather straightforward.
CreateMap<DBAccess.Models.Tables.GroupMemberAccount, GroupMemberAccount>();
CreateMap<DBAccess.Models.Tables.GroupMemberGroup, GroupMemberGroup>();
//Attempt 1: the not so straightforward. An explicit exhaustive mapping of everything, down to every single primitive type
CreateMap<DBAccess.Models.Tables.Group, Group>()
.ForMember(g => g.children, opts => opts.MapFrom(src => new GroupMembers
{
childAccounts = src.childAccounts!.Select(x => new GroupMemberAccount { parentGroupId = x.parentGroupId,
childAccountId = x.childAccountId,
childAccount = new Account { id = x.childAccount!.id,
name = x.childAccount!.name
}
}
).ToList(),
//childGroups = src.childGroups!.Select(x => new GroupMemberGroup(x)).ToList(),
childGroups = src.childGroups!.Select(x => new GroupMemberGroup { parentGroupId = x.parentGroupId,
childGroupId = x.childGroupId,
childGroup = new Group { id = x.childGroup!.id,
name = x.childGroup!.name
}
}
).ToList(),
}));
//Attempt 2: mapper injection
IMapper mapper = null!;
CreateMap<DBAccess.Models.Tables.Group, Group>()
.BeforeMap((_, _, context) => mapper = (IMapper)context.Items["mapper"]) //ADDING THIS LINE CAUSES ALL QUERIES TO LOOK FOR A NON EXISTENT Group.Groupid column
.ForMember(g => g.children, opts => opts.MapFrom(src => new GroupMembers
{
childAccounts = mapper.Map<List<DBAccess.Models.Tables.GroupMemberAccount>, List<GroupMemberAccount>>(src.childAccounts!),
childGroups = mapper.Map<List<DBAccess.Models.Tables.GroupMemberGroup>, List<GroupMemberGroup>>(src.childGroups!)
}))
}
}
Attempt1 will yield:
{
"id": "some guid",
"name": "some name"
"children": {}
}
even though the generated SQL does fetch all the required data to fill "children"
Attempt2 (mapper injection) is a technique I was suggested and have no clue how it is supposed to work. From what I gather, the mapping functions creates a few maps for some basic types while it uses its "future" self to create the remaining mappings, whenever it will be invoked in the future. Looks somehow like a one-time recursion.
However, it crashes as the generated SQL will look for a non-existent view column group.Groupid
SELECT [t].[id], [t].[name],
[g0].[parentGroupId], [g0].[childAccountId],
[g1].[parentGroupId], [g1].[childGroupId], [g1].[Groupid] -- where does [g1].[Groupid] come from??
FROM (
SELECT TOP(1) [g].[id], [g].[name]
FROM [HID_Rest].[group] AS [g]
WHERE [g].[id] = #__key_0
) AS [t]
LEFT JOIN [HID_Rest].[groupMemberAccount] AS [g0] ON [t].[id] = [g0].[parentGroupId]
LEFT JOIN [HID_Rest].[groupMemberGroup] AS [g1] ON [t].[id] = [g1].[parentGroupId]
ORDER BY ...
So regardless of the mapping profile I experimented with, what is the right mapping profile I need (and what ever else) to get the expected JSON output above? Or is this desired JSON structure possible at all?
After further work, I have figured that there was nothing wrong with my models and mapping. There's still something wrong though as the output to my GET requests is still incomplete. Here's the current new issue I need to deal with to solve this problem:
Issue with REST controller function Microsoft.AspNetCore.Mvc.ControllerBase.OK()?
I have a set of allowed Roles, and Users who can belong to a number of Roles.
I need to query both the number of Roles, and know from within the User entity to which Roles it belongs.
The first query works, the second query does not (no Roles are returned).
My code is below:
class User
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Index(IsUnique=true)]
public string Username { get; set; }
public ICollection<Role> Roles { get; set; }
public User()
{
this.Roles = new List<Role>();
}
}
class Role
{
[Key]
public int Id { get; set; }
[Index(IsUnique = true)]
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
class MemberContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
}
For the below example EF has created 3 databases:
**Roles {Id,Name}**
1, IdentityServerUsers
2, IdentityServerAdministrators
**UserRoles {User_Id,Role_Id}**
5179EF31-C7DD-E311-80BD-00155D458501, 2
**Users {Id,Name}**
5179EF31-C7DD-E311-80BD-00155D458501, admin
I want to be able to run the following queries:
using(var context = new MemberContext())
{
var roles = context.Roles.Where(x => x.Name == "IdentityServerAdministrators").FirstOrDefault();
// roles.Users is empty!!!
var user = context.Users.Where(x => x.Username == "admin").FirstOrDefault();
// user.Roles is empty!!
}
But the roles.Users and user.Roles both return empty, but looking at the above database tables they should have data.
Do I need to explicitly set mapping via EF Code First and what am I doing wrong please?
Update 1
I am now using virtual properties, but still null's are being returned:
class Role
{
[Key]
public int Id { get; set; }
[MaxLength(127)]
[Index(IsUnique = true)]
public string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
class User
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Index(IsUnique = true)]
[MaxLength(127)]
public string Name { get; set; }
}
using (var context = new MemberContext())
{
var unf1 = context.Roles.Where(x => x.Name == "IdentityServerAdministrators").FirstOrDefault();
var wow1 = unf1.Users;
// roles.Users is empty!!!
var unf2 = context.Users.Where(x => x.Username == "admin").FirstOrDefault();
var wow2 = unf2.Roles;
// user.Roles is empty!!
}
In your queries, you need to explicitly load the related entities since you're not using lazy loading.
using(var context = new MemberContext())
{
var roles = context.Roles.Include(x => x.Users).Where(x => x.Name == "IdentityServerAdministrators").FirstOrDefault();
// roles.Users is empty!!!
var user = context.Users.Include(x => x.Roles).Where(x => x.Username == "admin").FirstOrDefault();
// user.Roles is empty!!
}
If you want the related entities to be lazy loaded, the navigation properties must be virtual.
I have a WCF project and I generate the database with Entity framework v6 and code-first.
But I have a problem with the relation between the class User and FeedRss. I want several FeedRss for each User. My code work (no exception) but don't add in the ICollection feeds (in user), this list is empty after the recovery in the database.
public class User
{
[Key]
public int UserID {get; set;}
...
[InverseProperty("User")]
public ICollection<FeedRSS> feeds { get; set; }
public User()
{
feeds = new List<FeedRSS>();
}
}
one user->many feedRss
public class FeedRSS
{
[Key]
public int ID { get; set; }
...
public int UserId { get; set; }
[ForeignKey("UserId")]
public User User { get; set; }
public FeedRSS()
{
}
}
public class UsersContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<FeedRSS> Feeds { get; set; }
}
My function for test my code (but return a empty list) :
public User getUser(int Id)
{
using (UsersContext context = new UsersContext())
{
return context.Users.ToList().Find(
delegate(User u) {
return u.UserID == Id;
});
}
}
public List<FeedRSS> getFeedListTest(User u)
{
using (UsersContext ctx = new UsersContext())
{
User user = ctx.Users.First(i => i.UserID == u.UserID);
FeedRSS f = new FeedRSS() { name = "code", link = "uri" };
user.feeds.Add(f);
//the list user.feeds lenght is = 1
ctx.SaveChanges();
//update working
}
//get the same user in the database but the list uuu.feeds lenght is 0 :(
User uuu = this.getUser(u.UserID);
return uuu.feeds.ToList();
}
I tested other code very different (fluent API, force the UserId in FeedRss..) but I do not understand the principle of the relation in entity framework... I tried unsuccessfully several examples code...
*And sorry for my approximate English
You can either load feeds with Include(...) statement as CodeNotFound suggested or you can make the feeds collection virtual - that will enable lazy loading and EF will load feeds for you automatically on the fly.
public class User {
...
[InverseProperty("User")]
public virtual ICollection<FeedRSS> feeds { get; set; }
}
You can find a nice article about lazy loading and eager loading on the MSDN portal
I am using EF 4 Feature CTP 4.
I have the following database schema:
[Users] 1-M [UserRoles] M-1 [Roles]
I have a User and Role class (both POCOs).
When I try to associate an existing role to a user, a new record is getting inserted in the Roles table, instead of only inserting a new record in UserRoles.
So say I have User 1 and want to Associate with Role 2. When I try to save, I end up with a new record in Roles named "Role 2" with a record in UserRoles to the newly created Role. From the code below I was expecting only a new record in UserRoles with the mapping between User and Role.
Here is my POCO classes, mapping and test.
POCO Classes:
public class User {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public byte[] DataVersion { get; set; }
private List<Role> roles = new List<Role>();
public virtual IList<Role> Roles {
get {
return roles;
}
}
}
public class Role {
public int Id { get; set; }
public string Name { get; set; }
public List<User> Users { get; set; }
}
Mapping:
public class UserConfiguration : EntityConfiguration<Domain.User> {
public UserConfiguration() {
this.MapSingleType(user => new {
UserId = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
DataVersion = user.DataVersion
});
this.Property(u => u.DataVersion).IsConcurrencyToken().HasStoreType("timestamp").StoreGeneratedPattern = StoreGeneratedPattern.Computed;
//Users <--> Roles
this.HasMany(u => u.Roles).WithMany(r => r.Users)
.Map("UserRoles", (u, r) => new {
UserId = u.Id,
RoleId = r.Id
});
}
}
public class RoleConfiguration : EntityConfiguration<Domain.Role> {
public RoleConfiguration() {
this.MapSingleType(role => new {
RoleId = role.Id,
RoleName = role.Name
});
}
}
Test:
public void AssignRoleTest() {
var userId = 1;
User user;
var userRepo = new UsersRepository();
user = userRepo.GetUser(userId);
var roleRepo = new RolesRepository();
var roleId = 2;
var role = roleRepo.GetRole(roleId);
user.Roles.Add(role);
userRepo.SaveUser(user);
}
Code for Repository Save:
public void SaveUser(User user) {
if (user.Id > 0) {
dbContext.Users.Attach(user);
dbContext.MarkAsModified(user);
}
else {
dbContext.Users.Add(user);
}
Try to use context.DetectChanges() in your SaveUser method. It should work because you are using same context for both loading and saving entity graph.