first of all, sorry for my bad English
I have the following entities
public class User
{
public virtual int Id { get; set; }
public virtual Application Application { get; set; }
public User ()
{
Application = new Application ();
}
}
UserMap
public class UserMap : ClassMap<User>
{
public UserMap ()
{
Table ("Users");
Id (p => p.Id);
References (x => x.Application).Cascade.SaveUpdate ();
}
}
Application
public class Application
{
public virtual int Id { get; set; }
public virtual string ApplicationName { get; set; }
}
ApplicationMap
public ApplicationMap ()
{
Table ("Applications");
Id (x => x.Id);
Map (x => x.ApplicationName);
}
I received this json
{
"Application": 1,
}
and save the object this way
var user = new User();
user.Application.Id = Cast.To<int>(userModel.Application);
userService.Add(user);
userService.Commit();
Correctly records the data in the table "users" but left blank, the "ApplicationName" table field "application"
I think the error is in this line (user.Application.Id = Cast.To (userModel.Application);)
because I did not set "ApplicationName" field
but if I get the id as a parameter, I will need to get the application object by id, and assign the user object?.
thank you very much
You are right, conversion from ID into Entity (Applicaton) will require call to data layer and its operation GetById().
session.Load<Application>(id)
In cases, that we can be sure, that the passed Application ID exists, NHibernate has a dedicated way how to convert ID into its ENTITY. It is a Load() method, which does NOT hit the DB, just creates a proxy with provided ID, and the operation will succeed:
var user = new User();
var applicationId = Cast.To<int>(userModel.Application);
// behind is session.Load<Aplication>(applicaitonId)
var application = applicationService.Load(applicationId);
user.Application = application;
userService.Add(user);
session.Get<Application>(id)
The alternative is a Get() method, which always loads the instance by ID, i.e. hits the DB. If the ID does not match any ID, null is returned. Advantage in this scenario is, that we can even change the referenced ApplicaitonName (if application exists)
var user = new User();
var applicationId = Cast.To<int>(userModel.Application);
// behind is session.Get<Aplication>(applicaitonId)
var application = applicationService.GetById(applicationId);
if(application !== null)
{
application.ApplicationName = ... // here we can even change that;
user.Application = application;
}
cascade for new.
In case we would recieve brand new object
{
"Application": { "ApplicaitonName" : ... }
}
We can create one, and because of Cascading setting above, it will work as well
user.Application = new Appliation
{
ApplicationName = ...,
}
Related
The minimal project sources to reproduce the issue is here :
https://wetransfer.com/downloads/8d9325ce7117bb362bf0d61fc7c8571a20220708100401/326add
===================
This error is a classic; In layman's terms it is usually caused by a "bad" insertion when a navigation is not properly taken in account, causing a faulty Ef state somewhere.
Many solutions have been posted along the years but I fail to see how my specific scenario could cause the issue!
My schema is a many-to-many between Groups and Users. The middle entity is named GroupUser.
There's a twist : Each GroupUser has an owned entity containing extra data, DataPayload. This choice was made for versatility -- we wanted that payload to be stored in its own table.
Schema:
public class User {
public Guid Id { get; private set; }
public IList<GroupUser> GroupUsers { get; private set; } = new List<GroupUser>();
public User(Guid id) { Id = id; }
}
public class Group {
public Guid Id { get; private set; }
public Group(Guid id) { Id = id; }
}
public class GroupUser {
public Guid Id { get; private set; }
public Guid GroupId { get; private set; }
public Guid UserId { get; private set; }
// Navigation link
public Group? Group { get; private set; }
public DataPayload? Data { get; private set; }
public GroupUser(Guid groupId, Guid userId, DataPayload data) {
Id = Guid.NewGuid(); //Auto generated
UserId = userId;
GroupId = groupId;
Data = data;
}
// This extra constructor is only there to make EF happy! We do not use it.
public GroupUser(Guid id, Guid groupId, Guid userId) {
Id = id;
UserId = userId;
GroupId = groupId;
}
}
public class DataPayload {
//Note how we did not defined an explicit Id; we let EF do it as part of the "Owned entity" mechanism.
///// <summary>foreign Key to the Owner</summary>
public Guid GroupUserId { get; private set; }
public int DataValue { get; private set; }
public DataPayload(int dataValue) {
DataValue = dataValue;
}
public void SetDataValue(int dataValue) {
DataValue = dataValue;
}
}
To make it all work, we configure the navigations like this :
// --------- Users ---------
builder
.ToTable("Users")
.HasKey(u => u.Id);
// --------- Groups ---------
builder
.ToTable("Groups")
.HasKey(g => g.Id);
// --------- GroupUsers ---------
builder
.ToTable("GroupUsers")
.HasKey(gu => gu.Id);
builder
.HasOne<User>() //No navigation needed
.WithMany(u => u.GroupUsers)
.HasForeignKey(gu => gu.UserId);
builder
.HasOne<Group>(gu => gu.Group) //Here, we did define a navigation
.WithMany()
.HasForeignKey(gu => gu.GroupId);
builder
.OwnsOne(gu => gu.Data,
navBuilder => {
navBuilder.ToTable("PayloadDatas");
navBuilder.Property<Guid>("Id"); //Note: Without this EF would try to use 'int'
navBuilder.HasKey("Id");
//Configure an explicit foreign key to the owner. It will make our life easier in our Unit Tests
navBuilder.WithOwner().HasForeignKey(d => d.GroupUserId);
}
);
//.OnDelete(DeleteBehavior.Cascade) // Not needed (default behaviour for an owned entity)
Now, you know how everything is defined.
Basic setup : works!
var group = new Group(groupId);
await dbContext.Groups.AddAsync(group);
await dbContext.SaveChangesAsync();
var user = new User(userId);
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
Follow-up scenario : fails!
var groupUser = new GroupUser(groupId, userId, new DataPayload(dataValue: 777777));
user.GroupUsers.Add(groupUser);
await dbContext.SaveChangesAsync(); // Crash happens here!!!
Error:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.
I suspect that EF gets confused by the addition of two entities at once, where it has to compute some Ids itself : the new GroupUser and the DataPayload it contains. I'm not sure how it's supposed to know that it needs to give an Id to the GroupUser first and then use that Id as the foreign key in PayloadData. But that's just me; it might or might not be related to the issue.
But what do I need to change?
The mistake was in GroupUser's id-less constructor:
Id = Guid.NewGuid();
The code needs to let EF manage the keys when it comes to owned entities such as DataPayload which rely on a foreign key (GroupUserId) that's still in the making at the time of saving.
If you set a key value (Guid.NewGuid()) yourself, then EF gets confused between:
linking the new DataPayload entity to the GroupUser entity where you've shoehorned an Id value,
OR
just expecting an empty value (foreign key) and setting all the keys (both the GroupUser's Id and DataPayload's GroupUserId) itself.
All in all, EF feels like you announced that you were about to let it create 1 entity, but you've pulled the rug under its feet and done it yourself, so it ends up with 0 entity to work with. Hence the error message.
It should have been :
Id = Guid.Empty;
With Guid.Empty, EF clearly identifies that this entity is new and that has to be the same one as the one you told it to create and link to the new PayloadData -- that is, the instance that you've set in GroupUser.Data.
I try to use complex type if EF Core.
my table structure
source code
User
-----------
Id (uniqueidentifier)
FirstName (nvarchar(255))
LastName (nvarchar(255))
and my class strcuture is
public class UserId
{
public Guid Value { getl }
private UserId() { }
public UserId (Guid newId) {
//check and assign
}
}
public class User
{
public UserId Id { get; }
public Name Name { get; }
private User() { }
public User(Name name) {
//.... anti corruption
}
}
public class Name
{
public string First { get; }
public string Last { get; }
private Name() { }
public Name(string firstName, string lastName)
{
//anti curruption
}
}
and here is OnModelCreating
modelBuilder.Entity<User>()
.ToTable("User");
modelBuilder.Entity<User>()
.HasKey(u => u.Id);
modelBuilder.Entity<User>()
.OwnsOne(u => u.Name, un =>
{
un.Property(x => x.First).HasColumnName("FirstName");
un.Property(x => x.Last).HasColumnName("LastName");
});
every thing is work fined when I created a User and Save to database
but when I try to read it from database. the Name property is null
but again when I use .AsNoTracking. It work fined.
(I got from exception but I can't remember how to did it again)
MyDbContext db = new MyDbContext();
var newUser = new User(name: new Name("Foo", "Fighter"));
db.Users.Add(newUser);
db.SaveChanges();
var u1 = db.Users.Take(1).First();
PrintResult("Case 1", u1); //output > Could not read Name from database
var u2 = db.Users.AsNoTracking()
.Take(1).First();
PrintResult("Case 2", u2); //output > Read Name from database success
Console.Read();
my print result method
static void PrintResult(string label, User u)
{
Console.WriteLine($"{label} >>>>>");
if (u.Name == null)
{
Console.WriteLine("Could not read Name from database");
}
else
{
Console.WriteLine("Read Name from database success");
}
}
Can someone tell me that did I do something wrong ?
Why I have to use .AsNoTracking ?
In fact the issue does not reproduce with the posted model here. But it does with the one from the link, and the difference is the type of the PK property in the model - there is no problem when using well known primitive type, but you are using custom id class - yet another DDD "sugar", but with improper/missing equality implementation.
Without implementing value semantics for your id class, EF Core will compare it by reference, thus not finding the "owner PK" needed by the owned entity type. The no tracking queries have no such search, that's why it is "working".
The correct action is to implement value equality semantics in your UserId class (used as a type of User.Id property)
public class UserId
{
public Guid Value { get; }
protected UserId() { }
public UserId(Guid id)
{
if (id == Guid.Empty)
throw new ArgumentException("UserId must not be Empty");
Value = id;
}
}
for instance by adding
public override int GetHashCode() => Value.GetHashCode();
public override bool Equals(object obj) => obj is UserId other && Value == other.Value;
I have the following test that doesn't work:
public class DesktopDTO
{
public DesktopDTO() {}
public DesktopDTO(string title, Guid otherId)
{
Id = Guid.NewGuid();
Title = title;
OtherId = otherId;
}
public Guid Id { get; set; }
public string Title { get; set; }
public Guid OtherId { get; set; }
}
//setup environment:
MobileServiceClient mobileService = new MobileServiceClient("http://myserver.azurewebsites.net/");
IMobileServiceSyncTable<DesktopDTO> table = mobileService.GetSyncTable<DesktopDTO>();
if (!mobileService.SyncContext.IsInitialized)
{
var store = new MobileServiceSQLiteStore("localstore1.db");
store.DefineTable<DesktopDTO>();
await mobileService.SyncContext.InitializeAsync(store);
}
DesktopDTO input = new DesktopDTO("test124", Guid.NewGuid()); //this is my entity
//invoke action:
await table.InsertAsync(input);
//check results:
List<DesktopDTO> all = await table.ToListAsync(); //this returns 1 item
DesktopDTO r1 = all.Where(x => x.Id == input.Id).FirstOrDefault(); //this returns the created item
var query12 = await table.Where(x => x.Title == "test124").ToCollectionAsync(); //this returns 1 item
DesktopDTO r = (await table.Where(x => x.Id == input.Id).ToCollectionAsync()).FirstOrDefault(); //this returns null!!
The problem is that the last local query, which uses a Where() clause filtered by Id (which is the PK of the DesktopDTO entity), doesn't return the wanted entity.
The entity has been correctly INSERTed in the DB (as the other queries show, even the one filtered by "Title"), so I don't understand why the Where() filter should not work only with the PK.
I also tried using the LookupAsync() method, but again I got no results.
What am I doing wrong?
Thank you!
I try to reproduce the issue on my side. But I got the ArgumentException: "The id must be of type string".
If I change the Id type from Guid to string, I can't reproduce the issue that you mentioned. I works correctly on my side.
public class DesktopDTO
{
public DesktopDTO() { }
public DesktopDTO(string title, Guid otherId)
{
Id = Guid.NewGuid().ToString();
Title = title;
OtherId = otherId;
}
public string Id { get; set; }
public string Title { get; set; }
public Guid OtherId { get; set; }
}
Test Result:
For future reference, I discovered the problem.
Azure Mobile Service doesn't allow to have (natively) fields like GUIDs. But it accept Guids and silently transform them into Strings, using UPPER CASE.
Therefore, the solution is to turn all the Guids into UPPER CASE in the queries.
You can do either:
DesktopDTO r = (await table.Where(x => x.Id.ToString.ToUpper() == input.Id.ToString.ToUpper()).ToCollectionAsync()).FirstOrDefault();
or directly:
DesktopDTO r = await table.LookupAsync(id.ToString().ToUpper());
I've built a test application with Entity Framework to simulate a database that contains friends lists.
I want the database to store the user's ID's and when I retrieve them (the "AcceptedFriends") I want Entity Framework to also return the friends "usermodel".
But every time I try to add 2 users as friends to the "AcceptedFriends" table it
throws an exception:
" Violation of PRIMARY KEY constraint 'PK_Users'. Cannot insert duplicate key in object 'dbo.Users'. The duplicate key value is (GUID value of a user's ID) "
Some attempted solutions:
Solution 1
Attempting to create 2 lists of the same friend list (received, sent) but that defeats the purpose of what I am trying to achieve.
Solution 2
Here are the code files:
"Users Model"
public class Users
{
#region Private fields
#endregion
#region Public properties
public string Username { get; set; }
public string ID { get; set; }
public string PasswordHash { get; set; }
public virtual List<AcceptedFriends> AcceptedFriendsList { get; set; }
// public virtual List<PendingFriends> PendingFriendsList { get; set; }
// public virtual List<RemovedFriends> RemovedFriendsList { get; set; }
#endregion
}
"Accepted Friends model"
public class AcceptedFriends
{
#region Public properties
public string RelationKey { get; set; }
public string RequestSenderID { get; set; }
public string RequestReceiverID { get; set; }
public virtual List<Messages> ChatList { get; set; }
public Users RequestSender { get; set; }
public Users RequestReceiver { get; set; }
#endregion
}
"Database model creation"
#region Users table
// Create primary key in Users table
modelBuilder.Entity<Users>().HasKey(property => property.ID);
// Map Username to be unique
modelBuilder.Entity<Users>().HasIndex(property => property.Username).IsUnique();
// Create a one to many relation with AcceptedFriends table
modelBuilder.Entity<Users>()
.HasMany(property => property.AcceptedFriendsList)
.WithOne(property => property.RequestReceiver)
.HasForeignKey(property => property.RequestReceiverID);
#endregion
#region Accepted friends table
// Create key for AcceptedFriends
modelBuilder.Entity<AcceptedFriends>().HasKey(property => property.RelationKey);
#endregion
Edit
Here is how I am inserting the friends
public static void AddFriends(AcceptedFriends friends)
{
using(Context context = ConnectToDatabase())
{
context.AcceptedFriends.Add(friends);
context.SaveChanges();
};
}
Edit 2
Here is where I add the friends/users
Plus I've noticed another odd behaviour When I add new users to the friends table
without adding them to the users table first it adds them both to the friends table and users table.
Console.WriteLine("Connecting to database");
DB.ConnectToDatabase();
Console.WriteLine("Connected to database successfully");
List<Users> userList = new List<Users>(DB.GetUsersList());
List<AcceptedFriends> friendsCount = new List<AcceptedFriends>(DB.GetAcceptedFriends());
if(userList.Count != 2)
{
DB.AddUser(new Users()
{
Username = "User1",
PasswordHash = "PasswordHash",
});
DB.AddUser(new Users()
{
Username = "User2",
PasswordHash = "PasswordHash",
});
userList = new List<Users>(DB.GetUsersList());
};
if(friendsCount.Count < 1)
{
Users user1 = userList[0];
Users user2 = userList[1];
DB.AddFriends(new AcceptedFriends()
{
RequestReceiver = user2,
RequestSender = user1,
});
};
Console.WriteLine("Server is great success!");
Console.ReadLine();
Edit 3
I might have found a solution.
It does return the models both for the user and friends,
But I can't accept this as a solution yet because it feels to hackey(?) for me
(Thanks to #wertzui, You helped me to get to this solution)
Basically everytime a new context is created it sets up the the friends and users to return thier usermodels
/// <summary>
/// Gets the friends user models
/// </summary>
/// <param name="context"> The database context that was created </param>
private static void SetupFriends(Context context)
{
// For every "AcceptedFriend"
foreach(AcceptedFriends friend in context.AcceptedFriends)
{
// Get sender and receiver usermodels
// by matching ID's
Users sender = context.Users.FirstOrDefault(user => user.ID == friend.RequestSenderID);
Users receiver = context.Users.FirstOrDefault(user => user.ID == friend.RequestReceiverID);
sender.AcceptedFriendsList.Add(friend);
receiver.AcceptedFriendsList.Add(friend);
friend.RequestSender = sender;
friend.RequestReceiver = receiver;
};
}
When you create new User Instances in your new AcceptFriends {...} Code, you are not setting their Id, so they keep their default which is 0. Now Entity Framework thinks, that you want to create a new Friendship with 2 new Users. Instead you should populate them with the Users, you created earlier.
if(friendsCount.Count < 1)
{
Users user1 = userList[0];
Users user2 = userList[1];
DB.AddFriends(new AcceptedFriends()
{
RequestReceiver = user1,
RequestSender = user2,
});
}
I am using the Code First approach with my project.
Here is my object:
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get;set; }
}
If I create a new record it is OK:
using(var context = new context())
{
context.Accounts.add(account);
context.savechanges(); //This saves fine
}
But when I change a property it saves another record in the database:
using (var context = new context())
{
var account = context.Account.where(x => x.ID == GUID).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.savechanges(); // This creates a new record!!
}
}
I am fairly new to Entity framework; why is it creating new records each time? Is it the attribute on the ID? If it is, then how can I use GUIDS as IDs instead of integers?
The attribute in your Account option should work fine to set up the ID column as the primary key for your objects.
If you are getting a new entry added to the database when you save changes, it is almost certainly the result of you having changed the primary key (ID property) of the object after having received it from the DB. Maybe you are trying to set the GUID property yourself in some piece of code that you haven't included? (You should be letting the DB assign it).
In any case, this simple console app uses your setup and works as expected. If you don't see an obvious place in your code where you are changing the GUID, maybe you can post your actual code? (I notice a couple of typos in what is pasted in, so it doesn't appear to be what you are actually using)
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Account> Accounts { get; set; }
}
class Program
{
static Guid MyGuid = Guid.Empty;
static void Main(string[] args)
{
using (var context = new MyContext())
{
Account account = new Account { name = "OldName" };
context.Accounts.Add( account );
context.SaveChanges();
MyGuid = account.ID;
}
using (var context = new MyContext())
{
var account = context.Accounts.Where(x => x.ID == MyGuid).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.SaveChanges();
}
}
}
}