I wrote the following code to add a Person to a database, and I am questioning the need to check for an existing email (the unique key) before adding the person.
Will AddAsync throw an exception for me if I try to add a Person with the same email as an existing Person?
I've included a MVCE including my Service, Repository, and DbContext, but really this is just about the behavior of one call and whether or not it throws an Exception. If so, what exception is it?
namespace Project
{
public sealed class PersonService
{
private readonly PersonRepository personRepository;
public PersonService(PersonRepository personRepository)
{
this.personRepository = personRepository;
}
public async Task<Person> AddAsync(Person person)
{
var existingPerson = await this.personRepository.GetByEmailAsync(person.Email);
if (existingPerson != null)
{
throw new DuplicateEmailException(person.Email, $"The Email {person.Email} is already taken.");
}
await this.personRepository.AddAsync(person);
return person;
}
}
public sealed class PersonRepository
{
private readonly ProjectDbContext dbContext;
public PersonRepository(ProjectDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<Person> GetByEmailAsync(string email)
{
return await this.dbContext.Person
.FirstOrDefaultAsync(p => p.Email == email);
}
public async Task AddAsync(Person person)
{
if (person == null)
{
return;
}
await this.dbContext.AddAsync(person);
await this.dbContext.SaveChangesAsync();
}
}
public sealed class ProjectDbContext : DbContext
{
public ProjectDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Person> Person { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Person>()
.Property(p => p.Email)
.IsRequired();
modelBuilder.Entity<Person>()
.HasIndex(p => new { p.Email })
.IsUnique();
}
}
}
I am questioning the need to check for an existing email (the unique key) before adding the person. Will AddAsync throw an exception for me if I try to add a Person with the same email as an existing Person?
dbContext.AddAsync(person) will not throw any error but when _dbContext.SaveChanges(); will be called following exception will be thrown:
SqlException: Cannot insert duplicate key row in object 'dbo.Persons' with unique index 'IX_Perosns_Email'. The duplicate key value is (foo#gmail.com).
The statement has been terminated.
So you should validate/check this in controller or model level instead of in your PersonService.AddAsync() service method so that you can show the proper validation error message to the end user.
Related
I'm building some REST API server in .NET Core and using Postman software to test it. I have a problem with POST method which doesn't return me any value ("Could not get any response") when I try to perform second Add operation on my DBContext class inside CreateUser method. My code :
UsersController :
[Produces("application/json")]
[Route("api/[controller]")]
public class UsersController : Controller
{
private readonly DBContext _context;
#region CONSTRUCTOR
public UsersController(DBContext context)
{
_context = context;
}
#endregion
#region HTTP GET
// GET: api/users || api/users?cardnr=xxx
[HttpGet]
public async Task<IActionResult> GetUsers(string cardNr)
{
if (String.IsNullOrEmpty(cardNr))
{
try
{
var users = await _context.Users.ToListAsync();
if (users.Any())
{
return Json(users);
}
else
{
return NotFound();
}
}
catch (Exception ex)
{
Helpers.ExceptionLogger.LogException(ex);
return StatusCode(500);
}
}
else
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Cards.Any(c => c.CardNumber.Equals(cardNr)));
if (user == null)
{
return NotFound();
}
else
{
return new ObjectResult(user);
}
}
catch (Exception ex)
{
Helpers.ExceptionLogger.LogException(ex);
return StatusCode(500);
}
}
}
//GET: api/users/1
[HttpGet("{id}", Name = "GetUserByID")]
public async Task<IActionResult> GetUserByID(Int32 id)
{
try
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.IDUser == id);
if (user == null)
{
return NotFound();
}
else
{
return new ObjectResult(user);
}
}
catch (Exception ex)
{
Helpers.ExceptionLogger.LogException(ex);
return StatusCode(500);
}
}
#endregion
#region HTTP POST
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] Models.User userToCreate, string userGroupID)
{
if (userToCreate == null)
{
return BadRequest();
}
else
{
try
{
_context.Users.Add(userToCreate);
int parsingResult;
// if user passed userGroupID
if (userGroupID != null)
{
// parsing if userGroupID is a number
if (!int.TryParse(userGroupID, out parsingResult))
{
return BadRequest();
}
else
{
// if client want to assign a new user to some group
if (parsingResult > 0)
{
// creating new record in UserGroup table - assigning a user to group
var userGroup = new Models.UserGroup();
_context.Entry(userGroup).Property("IDGroup").CurrentValue = parsingResult;
_context.Entry(userGroup).Property("IDUser").CurrentValue = userToCreate.IDUser;
_context.UserGroups.Add(userGroup); // NOTE HERE
}
}
}
await _context.SaveChangesAsync();
return CreatedAtRoute("GetUserByID", new { id = userToCreate.IDUser }, userToCreate);
}
catch (Exception ex)
{
Helpers.ExceptionLogger.LogException(ex);
return StatusCode(500);
}
}
}
#endregion
}
User model :
public class User
{
[Key]
public int IDUser { get; set; }
[Required]
public string Name { get; set; }
public List<UserGroup> UsersGroups { get; set; }
}
UserGroup model :
public class UserGroup
{
public Group Group { get; set; }
public User User { get; set; }
}
DBContext class :
public class DBContext : DbContext
{
public DBContext(DbContextOptions<DBContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// shadow property - foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDUser");
// shadow property - foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDGroup");
modelBuilder.Entity<UserGroup>()
.HasKey( new string[]{ "IDUser", "IDGroup" });
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.Group)
.WithMany(g => g.UsersGroups)
.HasForeignKey("IDGroup");
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.User)
.WithMany(u => u.UsersGroups)
.HasForeignKey("IDUser");
base.OnModelCreating(modelBuilder);
}
public DbSet<Group> Groups { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserGroup> UserGroups { get; set; }
}
The problem lies in HttpPost method in UsersController.
When I do "normal" POST and pass JSON object which contain a user to add without assigning it to group (empty userGroupID parameter) everything is ok - user gets added to the DataBase and Postman returns me a user with its ID.
screen :
and when I try to add a new user but with adding it to specific group I always get an error :
screen :
Even despite that error new user gets properly added to DB and associated with its group (record gets added to UserGroup table; UserGroup is join table between Users and Groups table). So I have proper data in my DB but I always get this error and I can't return new added user to client who called API and can't get his ID. Am I doing something wrong in my CreateUser method ?
UPDATE :
I have added a comment line in "NOTE HERE" in CreateUser method in UsersController. If I comment whole this line I don't get an error from Postman but obviously I don't get my User associated with its group (I don't get new record added to UserGroup join table). So it seems like another Add method on context object causing an error ... Does it make sense ?
Did you try to debug it?
Set a breakpoint on the row:
if (userToCreate == null)
Send again the request with Postman and debug your app. There you can see what and where it goes wrong.
Please let me know how it is going so I know how can I help you:)
I have a many-to-many relationship (Users to skills), and when I try to associate an existing skill to a user, it always creates a new one.
User:
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
public virtual ICollection<Skill> Skills { get; set; }
}
Skill:
public class Skill
{
public int Id { get; set; }
public string Name { get; set; }
}
OnModelCreating:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>()
.HasMany<Skill>(user => user.Skills)
.WithMany();
}
Creating the association:
public ActionResult Skills(SkillsViewModel viewModel)
{
var user = UserManager.FindById(User.Identity.GetUserId());
if(!string.IsNullOrWhiteSpace(viewModel.NewSkill)
&& !user.Skills
.Where(sk => sk.Name == viewModel.NewSkill)
.Any())
{
var foundSkill = this.db.Skills
.Where(sk => sk.Name == viewModel.NewSkill)
.FirstOrDefault();
if(foundSkill != null)
{
user.Skills.Add(foundSkill);
}
else
{
user.Skills.Add(new Skill()
{
Name = viewModel.NewSkill
});
}
}
if(viewModel.SelectedSkillId > 0)
{
var foundSkill = this.db.Skills.Find(viewModel.SelectedSkillId);
user.Skills.Add(foundSkill);
}
this.UserManager.Update(user);
return RedirectToAction("skills");
}
I've stepped through, and verified that I do indeed get a 'foundSkill' from the database, but after I add it to the user, and save the user, the skill associated to the user is not the one I found, but a new one with the same name and different ID.
I figured it out. The UserManager was being loaded with one DbContext, and I was trying to associate a Skill loaded from a different DbContext.
Quick hack to test and fix this was to load the user from the same DbContext as the Skills, update the user, save the DbContext.
Longer term solution would be to ensure that everything uses the same DbContext per request.
I'm using EF6 code first with MVC 5. I have two objects, Movie and User, with each object having a collection of the other (many-to-many). Using an existing User, I'm trying to associate that User to a Movie but no rows are being inserted into the database. The Movie could be existing or new, but either way the association is not being created.
Movie is just a simple POCO
User inherits from IdentityUser
public class User : IdentityUser {
public virtual ICollection<Movie> Movies { get; set; }
public User() {
Movies = new Collection<Movie>();
}
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager) {
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
My Controller Action:
public async Task<HttpResponseMessage> Post(Movie rawMovie) {
try {
var movie = _store.Movies.Get(m => m.Id == rawMovie.Id).FirstOrDefault();
if (movie == null) {
movie = rawMovie;
_store.Movies.Insert(movie);
movie.Cast.Where(n => _store.Cast.Get(e => e.Id == n.Id).Select(e => e.Id).Contains(n.Id))
.ToList()
.ForEach(c => _store.Context.Entry(c).State = EntityState.Unchanged);
}
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if(user == null) return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid User");
user.Movies.Add(movie);
return Request.CreateResponse(_store.SaveChanges());
} catch (Exception e) {
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message);
}
}
I use the new IdentityDbContext as my single context, so it's used for both authentication and my POCO models - meaning that both Movie and User : IdentityUser share the same the context.
public class ApplicationContext : IdentityDbContext<User> {
public DbSet<Movie> Movies { get; set; }
public DbSet<Character> Cast { get; set; }
public ApplicationContext()
: base("MoovyConnection", throwIfV1Schema: false) { }
public static ApplicationContext Create() {
return new ApplicationContext();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Movie>().HasMany(m => m.Cast).WithMany(c => c.Movies)
.Map(t => t.MapLeftKey("Movid_Id").MapRightKey("Character_Id").ToTable("MovieCharacters"));
}
}
I've found this example but user.Movies does not have an attach method as it is only an ICollection.
What is the proper way to associate two objects to each other in a many-to-many relationship in EF6?
I'm a newbie with Entity Framework, and I have looked at the questions with the same title, but I haven't found a satisfying answer yet.
Here is my class:
public class MyUser
{
public string FirstName { get; set; }
public virtual ICollection<ProfileSkillEdu> Skills { get; set; }
}
And in the controller I have:
[HttpPost]
public ActionResult EditProfile(MyUser user, string emailAddress)
{
try
{
if (ModelState.IsValid)
{
_unitOfWork.GetMyUserRepository().Update(user);
_unitOfWork.Save();
return View(user);
}
}
catch (DataException)
{
//Log the error
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(user);
}
In my user repository:
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
I keep getting An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key. at dbSet.Attach(entityToUpdate). I watched the variables and I found that if MyUser has only 1 Skill object, it's fine because when it does Attach, the key is unique (value is 0). But if MyUser has 2 skill objects, both primary key have the value of 0 and therefore the error.
Can someone explain why all the Skill object's primary key have the values of 0? Also, is there a simple solution to this problem? I thought it's supposed to be straight forward but I have been struggling with this for hours.
Edit:
I wonder if the problem is because of how I use the context.
In the controller definition I have:
public class MyAccountController : Controller
{
IUnitOfWork _unitOfWork;
public ActionResult EditProfile()
{
if (_user == null)
{
MembershipUser currentUser = Membership.GetUser();
if (currentUser == null)
{
return RedirectToAction("Logon", "Account");
}
Guid currentUserId = (Guid)currentUser.ProviderUserKey;
MyUserService svc = new MyUserService();
MyUser user = svc.GetUserLoaded(currentUserId); //this uses include/eager loading to get the Skills too.
if (user == null)
{
return RedirectToAction("Logon", "Account");
}
else
_user = user;
}
return View(_user);
}
}
In my UnitOfWork I have:
public class UnitOfWork:IUnitOfWork
{
private GlobalContext context = new GlobalContext();
private GenericRepository<MyUser> myUserRepository;
private GenericRepository<Skill> skillRepository;
.. and implementation of Save() and Dispose()
}
Using EF 4.1 Code First, I have a Member entity and it in turn has two "one-to-many" relationships for a HomeAddress and WorkAddress. It also has a boolean property to state whether or not to use either of these addresses.
I have two issues that I can't figure out:
Whenever I update a member's address, a new record is added to the MemberAddresses table (with a new ID value) and the existing record is not deleted. Though it looks fine from the front-end perspective as the HomeAddressId and WorkAddressId in the parent Members table is updated with the new record, the old records are kept in the table (orhpaned). I don't want it to add a new address record when the address is updated. I only want it to update the existing record. If it has to add a new one, then I at least want it to clear out the old one.
There are times that I want to delete the address record from the table. For example, if the member previously had an associated HomeAddress and later the DontUseHomeAddress is set to true, I want the address to be deleted from the table. So far, I have tried setting it to null, but that just prevents any updates. It doesn't delete it.
I'm sure there just some code piece I'm missing, so I'm including all the relevant code below that I think might affect this.
public abstract class Entity
{
public int Id { get; set; }
}
public class Member : Entity
{
public string Name { get; set; }
public bool DontUseHomeAddress { get; set; }
public virtual MemberAddress HomeAddress { get; set; }
public bool DontUseWorkAddress { get; set; }
public virtual MemberAddress WorkAddress { get; set; }
//... other properties here ...
}
public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Members");
Property(m => m.Name).IsRequired().HasMaxLength(50);
//TODO: Somehow this is creating new records in the MemberAddress table instead of updating existing ones
HasOptional(m => m.HomeAddress).WithMany().Map(a => a.MapKey("HomeAddressId"));
HasOptional(m => m.WorkAddress).WithMany().Map(a => a.MapKey("WorkAddressId"));
}
}
public class MemberAddressMap : EntityTypeConfiguration<MemberAddress>
{
public MemberAddressMap()
{
ToTable("MemberAddresses");
Property(x => x.StreetAddress).IsRequired().HasMaxLength(255);
Property(x => x.City).IsRequired().HasMaxLength(50);
Property(x => x.State).IsRequired().HasMaxLength(2);
Property(x => x.ZipCode).IsRequired().HasMaxLength(5);
}
}
Here is the InsertOrUpdate method from my repository class that my controller calls:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : Entity
{
private readonly EfDbContext _context;
private readonly DbSet<TEntity> _dbSet;
public Repository(EfDbContext context)
{
_context = context;
_dbSet = _context.Set<TEntity>();
}
public bool InsertOrUpdate(TEntity entity)
{
if(entity.Id == 0)
{
_dbSet.Add(entity);
}
else
{
_context.Entry(entity).State = EntityState.Modified;
}
_context.SaveChanges();
return true;
}
//... Other repository methods here ...
}
EDIT: Adding in code for UnitOfWork and MemberServices
public class MemberServices : IMemberServices
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository _memberRepository;
public MemberServices(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_memberRepository = unitOfWork.RepositoryFor<Member>();
}
public Member Find(int id)
{
return _memberRepository.FindById(id);
}
public bool InsertOrUpdate(Member member)
{
// if(member.HomeAddress != null)
// _unitOfWork.SetContextState(member.HomeAddress, EntityState.Modified);
//
// if(member.WorkAddress != null)
// _unitOfWork.SetContextState(member.WorkAddress, EntityState.Modified);
//
// if(member.DontUseHomeAddress)
// {
// //TODO: This is an attempted hack... fix it by moving somewhere (possibly to repository)
// var context = new EfDbContext();
// context.Set<MemberAddress>().Remove(member.HomeAddress);
// context.SaveChanges();
// }
_memberRepository.InsertOrUpdate(member);
return true;
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly EfDbContext _context;
public UnitOfWork()
{
_context = new EfDbContext();
}
public IRepository<T> RepositoryFor<T>() where T : Entity
{
return new Repository<T>(_context);
}
public void Attach(Entity entity)
{
_context.Entry(entity).State = EntityState.Unchanged;
}
public void SetContextState(Entity entity, EntityState state)
{
_context.Entry(entity).State = state;
}
public void Save()
{
_context.SaveChanges();
}
}
Setting the state _context.Entry(entity).State = EntityState.Modified; doesn't affect the state of related entities. If you want to take care of changes of your related entities you must set their state to Modified as well:
if (member.HomeAddress != null)
_context.Entry(member.HomeAddress).State = EntityState.Modified;
if (member.WorkAddress != null)
_context.Entry(member.WorkAddress).State = EntityState.Modified;
_context.Entry(member).State = EntityState.Modified;
This is not generic anymore.
To delete an entity you have to call the appropriate method to delete an entity; setting the navigation property to null is not enough:
_context.MemberAddresses.Remove(member.HomeAddress);