I am implementing Chat functionality excatly like this one in asp.net.This is the model explaination
My classes are: User, Conversaiton and Message:
public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
public string UserName { get; set; }
//keys
public ICollection<Conversation> Conversations { get; set; }
}
public class Conversation
{
public Guid ID { get; set; }
public ICollection<Message> Messages { get; set; }
public User RecipientUser { get; set; }
public Guid SenderUserId { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
}
and finally,
public class Message
{
public int ID { get; set; }
public string Text { get; set; }
public bool IsDelivered { get; set; }
public bool IsSeen { get; set; }
public Guid SenderUserId { get; set; }
public Conversation Conversation { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
}
I am using EntityTypeConfiguration using fluent Api which are:
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
HasMany(x => x.Conversations).WithRequired(x => x.RecipientUser).HasForeignKey(x => x.SenderUserId);
}
}
public class ConversationConfig : EntityTypeConfiguration<Conversation>
{
public ConversationConfig()
{
HasKey(x => x.ID);
HasRequired(x => x.RecipientUser).WithMany(x => x.Conversations);
HasMany(x => x.Messages).WithRequired(x => x.Conversation).HasForeignKey(x => x.SenderUserId);
}
}
But I'm getting error as follows:
Multiplicity constraint violated. The role 'Conversation_RecipientUser_Target' of the relationship 'DataAcessLayer.Conversation_RecipientUser' has multiplicity 1 or 0..1.
If anyone could help me. Many thanks!!!
I didn't get the exception when I used your model, but that doesn't matter much, because you've got to make some changes anyway.
First, this configuration in UserConfig ...
HasMany(x => x.Conversations).WithRequired(x => x.RecipientUser)
.HasForeignKey(x => x.SenderUserId);
... doesn't make sense to me. This means that the conversations belong to the recipient, not to the sender, which in itself is somewhat unexpected. But the name of the foreign key is SenderUserId. If you save objects into this model, SenderUserId will get the value of the recipient's ID!
Second, assuming the previous point was an error, you make it harder than necessary by defining ...
public User RecipientUser { get; set; }
public Guid SenderUserId { get; set; }
This means that you can only navigate from Conversation to the sender User by an explicit join over SenderUserId. Conversely, you can only set the recipient by setting RecipientUser, not by simply setting a foreign key value.
Third, you shouldn't have this SenderUserId in Message. It should be ConversationId instead. You can find a Message's sender through the Conversation it belongs to.
All in all, I changed your model into this, reducing it to the bare-bone minimum, and using int for ID, just because it's easier reading:
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Conversation> Conversations { get; set; }
}
public class Conversation
{
public int ID { get; set; }
public int SenderUserID { get; set; }
public User SenderUser { get; set;}
public int RecipientUserID { get; set; }
public User RecipientUser { get; set; }
public ICollection<Message> Messages { get; set; }
}
public class Message
{
public int ID { get; set; }
public string Text { get; set; }
public int ConversationID { get; set; }
public Conversation Conversation { get; set; }
}
And the only configuration:
public class ConversationConfig : EntityTypeConfiguration<Conversation>
{
public ConversationConfig()
{
HasKey(c => c.ID);
HasRequired(c => c.SenderUser).WithMany(u => u.Conversations)
.HasForeignKey(c => c.SenderUserID).WillCascadeOnDelete(true);
HasRequired(c => c.RecipientUser).WithMany()
.HasForeignKey(c => c.RecipientUserID).WillCascadeOnDelete(false);
HasMany(c => c.Messages).WithRequired(x => x.Conversation)
.HasForeignKey(x => x.ConversationID);
}
}
(One of the foreign keys doesn't have cascaded delete because that will cause a SQL-Server error: multiple cascade paths).
A simple test:
using (DbContext db = new DbContext(connectionString))
{
var send = new User { Name = "Sender" };
db.Set<User>().Add(send);
var rec = new User { Name = "Recipient" };
var messages = new[] { new Message { Text = "a" }, new Message { Text = "b" } };
var conv = new Conversation { SenderUser = send, RecipientUser = rec, Messages = messages };
db.Set<User>().Add(rec);
db.Set<Conversation>().Add(conv);
db.SaveChanges();
}
Result:
Users:
ID Name
1 Recipient
2 Sender
Converstions:
ID RecipientUserID SenderUserID
11 1 2
Messages:
ID Text ConversationID
21 a 11
22 b 11
Given Conversation has 1 User foreign key-column which called SenderUserId
// Here you set the user key for the old user!
conversation.RecipientUser = user;
// Here you are trying to change Conversation.SenderUserId from other user (currentUser)
currentUser.Conversations.Add(conversation);
As you seen the added user into collection is not the same which you have assigned to the Conversation 1..n can not be working in this way correctly.
EF try to say to you with the exception, that you are trying to add/change more then entity for the releshinship one-to-zero-or-one, the error message is a little bit foggy I would say the error message should be like "One-to-many navigation properties should using the same entity"
solution 1)
// conversation.RecipientUser = user; remove this line why you need it?!
solution 2)
Create a new property in conversation one for CurrentUser and the other one for User
public class User
{
public User()
{
Conversations = new List<Conversation>();
CurrentUserConversations = new List<Conversation>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
public string UserName { get; set; }
public ICollection<Conversation> Conversations { get; set; }
public ICollection<Conversation> CurrentUserConversations { get; set; }
}
public class Conversation
{
public Conversation()
{
Messages = new List<Message>();
}
public Guid ID { get; set; }
public User RecipientUser { get; set; }
public Guid SenderUserId { get; set; }
public User CurrentUser { get; set; }
public Guid? CurrentUserId { get; set; }
public ICollection<Message> Messages { get; set; }
}
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
HasMany(x => x.Conversations).WithRequired(x => x.RecipientUser).HasForeignKey(x => x.SenderUserId);
HasMany(x => x.CurrentUserConversations).WithOptional(x => x.CurrentUser).HasForeignKey(x => x.CurrentUserId);
}
}
How to use it:
var user = myDbContext.Users.First();
var currentUser = new User { Active = true, Id = Guid.NewGuid() };
myDbContext.Users.Add(currentUser);
var message = new Message { IsDelivered = true };
var conversation = new Conversation() { ID = Guid.NewGuid() };
conversation.Messages.Add(message);
conversation.CurrentUser = user;
currentUser.Conversations.Add(conversation);
myDbContext.Conversations.Add(conversation);
myDbContext.SaveChanges();
Related
In an ASP.NET core (dotnet 6) web application, we have a small user management functionality in which a user can be assigned to a specific site (physical location) with certain rights. A user can be assigned to one or more sites and a site can also be assigned to one or more users, thus the many to many relationship. We have the following classes:
public class User
{
public int ID { get; set; }
public string SamAccountName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int GroupID { get; set; }
public bool IsEnabled { get; set; }
public ICollection<UserSite> UserSites { get; set; }
}
public class Site
{
public int ID { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public ICollection<UserSite> UserSites { get; set; }
}
The many to many relationship is described in a join table with fluent API:
public class UserSite
{
public int UserID { get; set; }
public User User { get; set; }
public int SiteID { get; set; }
public Site Site { get; set; }
}
// Fluent API for UserSite table
builder.HasKey(us => new { us.UserID, us.SiteID });
builder.HasOne(us => us.User)
.WithMany(us => us.UserSites)
.HasForeignKey(us => us.UserID)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(us => us.Site)
.WithMany(us => us.UserSites)
.HasForeignKey(us => us.SiteID)
.OnDelete(DeleteBehavior.Cascade);
These classes result in the following DTO's:
public class UserDTO
{
public int ID { get; set; }
public string SamAccountName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int GroupID { get; set; }
public bool IsEnabled { get; set; }
public IList<SiteDTO> Sites { get; set; }
}
public class SiteDTO
{
public int ID { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
What we want to know is, what is the best way to create a new user or update an existing user? Just to clarify, the data in the Sites table exists already and is static. What I mean by that is that when creating a new user or updating an existing one, a user can be assigned to one or more existing sites, but no new site is created in the process.
We already tried some things that did work but we are not sure if this is really the best way to this or if there is a far better way to do it. Here is what we tried:
The create method
// Create method
public IActionResult CreateUser(UserDTO userDTO)
{
if (ModelState.IsValid)
{
User user = _mapper.Map<User>(userDTO);
user.IsEnabled = true;
AttachSitesToUser(user.ID, user.UserSites);
_context.Users.Add(user);
_context.SaveChanges();
userViewModel = _mapper.Map<UserViewModel>(user);
}
return new JsonResult(new[] { userDTO });
}
The update method:
// Update method
public IActionResult UpdateUser(UserDTO userDTO)
{
if (ModelState.IsValid)
{
User user = _context.Users
.Include(u => u.Sites)
.ThenInclude(s => s.Site)
.Single(u => u.ID == userDTO.ID);
_mapper.Map(userViewModel, user);
AttachSitesToUser(user.ID, user.UserSites);
_context.Entry(user).State = EntityState.Modified;
_context.SaveChanges();
}
return new JsonResult(new[] { userDTO });
}
The helper method AttachSitesToUser:
// Helper method that adds the user ID and attaches the site(s) to the context
private void AttachSitesToUser(int userID, IEnumerable<UserSite> userSites)
{
foreach (UserSite userSite in userSites)
{
userSite.UserID = userID;
if (Context.Entry<Site>(userSite.Site).State == EntityState.Detached)
{
Context.Sites.Attach(userSite.Site);
Context.Entry<Site>(userSite.Site).State = EntityState.Unchanged;
}
}
}
As you can see, for the moment we have to loop through the UserSites list in the User entity and attach the sites the user was assigned to manually to the context. Is there no better way to do this or is this the official best practice?
I'm trying to store older versions of entities in my database. To do that I am copying the existing values before I update them. For some reason EF Core won't let me use the same batch.Values property twice.
public async Task<Batch> UpdateBatch(Batch batch, Batch updatedBatch)
{
foreach (var valueParameter in batch.Values)
{
batch.ValuesHistory.Add(new ParameterValueHistory
{
Parameter = valueParameter.Parameter,
ParameterBatchNumber = valueParameter.ParameterBatchNumber,
Value = valueParameter.Value
});
}
batch.Values = updatedBatch.Values;
batch.Version++;
await this.context.SaveChangesAsync();
return batch;
}
The foreach loop and batch.Values = updatedBatch.Values; work exactly like they should when only one of them exists. But whenever they're both active I get the following error:
The instance of entity type 'ParameterValue' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
These are the relevant models:
ParameterValue:
public class ParameterValue
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public virtual RecipeParameter Parameter { get; set; }
public string Value { get; set; }
public string? ParameterBatchNumber { get; set; }
}
ParameterValueHistory:
public class ParameterValueHistory
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public virtual RecipeParameter Parameter { get; set; }
public string Value { get; set; }
public string? ParameterBatchNumber { get; set; }
}
RecipeParameter for context:
public class RecipeParameter
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Type { get; set; }
public string Unit { get; set; }
public string Value { get; set; }
public bool BatchRequired { get; set; }
}
Batch:
public class Batch
{
[Key]
[MaxLength(12)]
public string BatchNumber { get; set; }
public virtual List<ParameterValue> Values { get; set; }
public virtual List<ParameterValueHistory> ValuesHistory { get; set; }
public int Version { get; set; }
[Required]
public bool IsResearch { get; set; }
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
}
This is my DbContext class:
public class ApplicationDataContext : DbContext
{
public ApplicationDataContext(DbContextOptions<ApplicationDataContext> options)
: base(options)
{
}
public DbSet<Product> Product { get; set; }
public DbSet<Batch> Batch { get; set; }
public DbSet<ParameterValue> ParameterValue { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
base.OnConfiguring(optionsBuilder);
}
}
Why does this error keep showing up? Even when I am just accessing the propety as batch.Values more than once, it gives me this error.
UPDATE:
This is the controller method that calls the UpdateBatch method.
[HttpPut("{productId}/batches/{batchNumber}")]
public async Task<ActionResult<Batch>> PutBatch(string batchNumber, Batch updatedBatch)
{
Batch batch = await this.repository.GetBatchByBatchNumber(batchNumber);
if (batch == null)
{
return NotFound()
}
return await this.repository.UpdateBatch(batch, updatedBatch);
}
When you use batch.Values = updatedBatch.Values;, because batch.Values contains the foreign key of Batch, and if the value in updatedBatch.Values also contains the key value,if the equal operation is performed directly, due to the foreign key constraint, the foreign key cannot be modified directly, which will cause your error.
Therefore, you cannot include the key value in the Values in your updateBatch.
Regarding your question. I did a simple test. You can see the following code(updateBatch.Values have no Id).
var batch = _context.Batches.Include(c => c.Values)
.ThenInclude(c => c.Parameter)
.Include(b => b.ValuesHistory)
.ThenInclude(c => c.Parameter)
.Where(c => c.BatchNumber == "1")
.FirstOrDefault();
var updateBatch = new Batch
{
Version = 3,
CreatedOn = new DateTime(),
IsResearch = true,
Values = new List<ParameterValue>
{
new ParameterValue
{
Value = "hello",
Parameter = new RecipeParameter
{
BatchRequired = true,
Name = "h",
Type = "e",
Unit = "l",
Value = "o"
}
},
},
ValuesHistory = new List<ParameterValueHistory>()
};
foreach (var valueParameter in batch.Values)
{
batch.ValuesHistory.Add(new ParameterValueHistory
{
Parameter = valueParameter.Parameter,
ParameterBatchNumber = valueParameter.ParameterBatchNumber,
Value = valueParameter.Value
});
}
batch.Values = updateBatch.Values;
batch.Version++;
_context.SaveChanges();
Test result:
start by making these changes..
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
should not be on
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
instead model like
public class Batch
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
//you can add index on this
[MaxLength(12)]
public string BatchNumber { get; set; }
public int Version { get; set; }
[Required]
public bool IsResearch { get; set; }
[Required]
public DateTime CreatedOn { get; set; };// set this in the repo or create do another way
//you add this but don't see the linkage aka ParameterValue does not have a BatchId
public virtual List<ParameterValue> Values { get; set; }
public virtual List<ParameterValueHistory> ValuesHistory { get; set; }
}
I have a annoying problem with my code.
My model :
public class Option
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Conference> Conference { set; get; }
}
public partial class Conference
{
[Key, ForeignKey("User")]
public int UserId { get; set; }
public virtual Option Option { set; get; }
public virtual User User { get; set; }
}
public partial class User
{
public int Id {get; set; }
public string Name { get; set; }
public virtual Conference Conference { get; set; }
}
And now i`m getting Option object from Db by dbSet.Find(id) (RepositoryFactory) and what i want to do is to save newly created User, but with selected Option.
If i do like that:
var option = dbSet.Find(id);
var user = new User()
{
Name = "Name",
Conference = new Conference
{
Option = option
}
};
//...
context.SaveChanges();
I`m getting an exception:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
What I`m doing wrong?
Edit: I Tried to create Mapping, but this doesn`t seems to work too.
modelBuilder.Entity<Conference>()
.HasKey(x => x.UserId)
.HasRequired(x => x.User)
.WithOptional(user => user.Conference);
modelBuilder.Entity<Option>()
.HasMany(option => option.Conferences)
.WithRequired(conference => conference.Option)
.HasForeignKey(conference => conference.UserId);
Are you trying to achieve a 1:1 relationship between User and Conference? If so, you need to add an Id (key) property to User. Please see the comments I added to the code sample below regarding the 1:1 relationship. I would advise further evaluation of your domain layer to ensure this is what you are trying to achieve...
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
namespace Stackoverflow
{
public class EntityContext : DbContext
{
public IDbSet<Conference> Conferences { get; set; }
public IDbSet<Option> Options { get; set; }
public IDbSet<User> Users { get; set; }
}
public class Option
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Conference> Conference { set; get; }
}
public class Conference
{
// In one-to-one relation one end must be principal and second end must be dependent.
// User is the one which will be inserted first and which can exist without the dependent one.
// Conference end is the one which must be inserted after the principal because it has foreign key to the principal.
[Key, ForeignKey("User")]
public int UserId { get; set; }
public int OptionId { get; set; }
public virtual Option Option { set; get; }
public virtual User User { get; set; }
}
public class User
{
// user requires a key
public int Id { get; set; }
public string Name { get; set; }
public virtual Conference Conference { get; set; }
}
class Program
{
static void Main(string[] args)
{
using (var entityContext = new EntityContext())
{
// added to facilitate your example
var newOption = new Option {Name = "SomeOptionName"};
entityContext.Options.Add(newOption);
entityContext.SaveChanges();
var option = entityContext.Options.Find(newOption.Id);
var user = new User
{
Name = "Name",
Conference = new Conference
{
Option = option
}
};
// need to add the user
entityContext.Users.Add(user);
//...
entityContext.SaveChanges();
}
}
}
}
I have two models and a dbcontext:
public class User
{
public int UserId { get; set; }
public String UserName { get; set; }
public virtual ICollection<Conversation> Conversations { get; set; }
}
public class Conversation
{
public int ConversationId { get; set; }
public String Text { get; set; }
public int UserId { get; set; }
public virtual User User { get; set; }
}
public class ChatContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Conversation> Conversations { get; set; }
}
I am trying to add a conversation entity so I need to retrieve the UserId from User table. So far I have:
var newtext = new Conversation()
{
Text = message, // message is sent from user
UserId = ChatContext.User.Where(u => u.UserName == name).Select(u => u.UserId)
};
I need to query the user table to retrieve the userId associated with that particular name. How should I achieve this? The error is:
ChatContext does not have a definition for user.
Firstly: You must add static to Users like this:
public class ChatContext : DbContext
{
public static DbSet<User> Users { get; set; }
public DbSet<Conversation> Conversations { get; set; }
}
Secondly: You must add FirstOrDefault method to your query, Because it will return an iQueryable like this:
var newtext = new Conversation()
{
Text = message, // message is sent from user
UserId = ChatContext.Users.FirstOrDefault(u => u.UserName == name).UserId
};
This should works.
I have two API calls. GetExam and SaveExam. GetExam serializes to JSON which means by the time I go to save, the entity is detached. This isnt a problem, I can go retrieve the entity by its primary key and update its properties manually.
However, when I do so the exam questions get its current collection duplicated. For example, if examToSave.ExamQuestions had a few questions deleted, and a new one added all selectedExam.exam_question are duplicated and the new one is added in. Eg. if 3 questions existed, I deleted 1 and added 4 there will now be 7.
Domain models:
public partial class exam
{
public exam()
{
this.exam_question = new HashSet<exam_question>();
}
public int ID { get; set; }
public string ExamName { get; set; }
public string ExamDesc { get; set; }
public Nullable<decimal> TimeToComplete { get; set; }
public bool AllowBackStep { get; set; }
public bool RandomizeAnswerOrder { get; set; }
public int Attempts { get; set; }
public virtual ICollection<exam_question> exam_question { get; set; }
}
public partial class exam_question
{
public exam_question()
{
this.exam_answer = new HashSet<exam_answer>();
}
public int ID { get; set; }
public int ExamID { get; set; }
public string QuestionText { get; set; }
public bool IsFreeForm { get; set; }
public virtual exam exam { get; set; }
public virtual ICollection<exam_answer> exam_answer { get; set; }
}
public partial class exam_answer
{
public int ID { get; set; }
public string AnswerText { get; set; }
public int QuestionID { get; set; }
public bool IsCorrect { get; set; }
public virtual exam_question exam_question { get; set; }
}
Save method:
[Route("SaveExam")]
[HttpPost]
public IHttpActionResult SaveExam(ExamViewModel examToSave)
{
using (var db = new IntranetEntities())
{
// try to locate the desired exam to update
var selectedExam = db.exams.Where(w => w.ID == examToSave.ID).SingleOrDefault();
if (selectedExam == null)
{
return NotFound();
}
// Redacted business logic
// Map the viewmodel to the domain model
Mapper.CreateMap<ExamAnswerViewModel, exam_answer>();
Mapper.CreateMap<ExamQuestionViewModel, exam_question>().ForMember(dest => dest.exam_answer, opt => opt.MapFrom(src => src.QuestionAnswers));
Mapper.CreateMap<ExamViewModel, exam>().ForMember(dest => dest.exam_question, opt => opt.MapFrom(src => src.ExamQuestions));
var viewmodel = Mapper.Map<exam>(examToSave);
// Update exam properties
selectedExam.ExamName = viewmodel.ExamName;
selectedExam.ExamDesc = viewmodel.ExamDesc;
selectedExam.AllowBackStep = viewmodel.AllowBackStep;
selectedExam.Attempts = viewmodel.Attempts;
selectedExam.RandomizeAnswerOrder = viewmodel.RandomizeAnswerOrder;
selectedExam.exam_question = viewmodel.exam_question; // DUPLICATES PROPS
// Save
db.SaveChanges();
return Ok(examToSave);
}
}