Updating an Entity in EF with a 1:1 relationship - c#

I have an application with 4 layers:
-Core (Models)
-Repository (EF DbContext actions)
-Service (Business logic)
-Web (MVC)
I'm trying to update an object with a 1:1 relationship with EF using the following method:
public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
{
var product = await GetProductByIdAsync(ticketing.ProductId);
// Validation removed for simplicity
// 'ticketing' passed validation so let's
// just replace it with the existing record.
product.Ticketing = ticketing;
_repo.ProductRepository.Update(product);
return await _repo.SaveAsync();
}
This works for an initial insert, but it doesn't work as I'd expect when I'm updating the record:
A first chance exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred...
The actual error message is:
Violation of PRIMARY KEY constraint 'PK_dbo.ProductTicketing'. Cannot insert duplicate key in object 'dbo.ProductTicketing'. The statement has been terminated.
Obviously the PK and FK "ProductId" doesn't change - so why does EF try to drop and insert my record instead of just updating it, and why does it fail?
But more importantly - how can I prevent this. I know I can manually map the object values and then update it - that works but it's tedious mapping two identical objects together and doesn't feel correct.
My repository for retrieving the Product object is in my Repository layer, while the method above is in my Service layer.
This is how I'm currently resolving this - and it looks as dirty as it feels:
public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
{
var product = await GetProductByIdAsync(ticketing.ProductId);
// Validation removed for simplicity
if (product.Ticketing == null)
{
product.Ticketing = ticketing;
}
else
{
product.Ticketing.AllowEventBooking = ticketing.AllowEventBooking;
// Doing the same for all other properties etc
// etc
// etc
}
_repo.ProductRepository.Update(product);
return await _repo.SaveAsync();
}
How can I achieve this without doing all this horrible mapping an object to an identical object?
Edit
Here are the two models referred to above:
[Table(#"Products")]
public class Product
{
[Key]
public int Id { get; set; }
public virtual ProductTicketing Ticketing { get; set; }
// Removed others for clarity
[Timestamp]
public byte[] RowVersion { get; set; }
}
[Table(#"ProductTicketing")]
public class ProductTicketing
{
[Key, ForeignKey("Product")]
public int ProductId { get; set; }
public bool AllowEventBooking { get; set; }
// Removed others for clarity
public virtual Product Product { get; set; }
}
It's also probably worth noting that the "ProductTicketing" object I'm passing into the UpdateProductTicketing method is a new object created from values in my controller - but the ID is the same so I assume it should work.

I think I see the problem now - when you do product.Ticketing = ticketing;, EF treats this as a new insert.
To avoid this, you can do one of these things:
Continue using the workaround (which is not a wokaround actually but just the way EF expects you to tell when to insert vs. when to update).
Now this depends on rest of your code and design, but instead of fetching the product, you can fetch the ticket and update its properties. Of course, this means that if the ticketing is not found, you need to insert it which then kinda looks like what you're already doing with UpdateProductTicketing.
Use the InsertOrUpdate pattern (I made some assumptions about your code but hopefully it gives you the idea - the main thing here is the InsertOrUpdate method):
public class ProductRepository : IRepository
{
private SomeContext context;
public void InsertOrUpdate(ProductTicketing ticketing)
{
context.Entry(ticketing).State = ticketing.ProductId == 0 ?
EntityState.Added :
EntityState.Modified;
context.SaveChanges();
}
}
// And a generic version
public void InsertOrUpdate<T>(T entity) where T : class
{
if (context.Entry(entity).State == EntityState.Detached)
context.Set<T>().Add(entity);
context.SaveChanges();
}

You are getting that error because ef thinks that the ProductTicket is a new entity and is trying to insert the entity into the db. I don't know about the _repo.ProductRepository.Update(product) call but how about you attach the ProductTicket to the context and set the entity state to modified

Related

Entity Framework Core Find and Update single value [duplicate]

What is the best approach to update database table data in Entity Framework Core?
Retrieve the table row, do the changes and save
Use keyword Update in DB context and handle exception for item not exist
What are the improved features we can use over EF6?
To update an entity with Entity Framework Core, this is the logical process:
Create instance for DbContext class
Retrieve entity by key
Make changes on entity's properties
Save changes
Update() method in DbContext:
Begins tracking the given entity in the Modified state such that it will be updated in the database when SaveChanges() is called.
Update method doesn't save changes in database; instead, it sets states for entries in DbContext instance.
So, We can invoke Update() method before to save changes in database.
I'll assume some object definitions to answer your question:
Database name is Store
Table name is Product
Product class definition:
public class Product
{
public int? ProductID { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
public decimal? UnitPrice { get; set; }
}
DbContext class definition:
public class StoreDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Your Connection String");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
// Set key for entity
entity.HasKey(p => p.ProductID);
});
base.OnModelCreating(modelBuilder);
}
}
Logic to update entity:
using (var context = new StoreDbContext())
{
// Retrieve entity by id
// Answer for question #1
var entity = context.Products.FirstOrDefault(item => item.ProductID == id);
// Validate entity is not null
if (entity != null)
{
// Answer for question #2
// Make changes on entity
entity.UnitPrice = 49.99m;
entity.Description = "Collector's edition";
/* If the entry is being tracked, then invoking update API is not needed.
The API only needs to be invoked if the entry was not tracked.
https://www.learnentityframeworkcore.com/dbcontext/modifying-data */
// context.Products.Update(entity);
// Save changes in database
context.SaveChanges();
}
}
According to Microsoft docs:
the read-first approach requires an extra database read, and can result in more complex code for handling concurrency conflict
However, you should know that using Update method on DbContext will mark all the fields as modified and will include all of them in the query. If you want to update a subset of fields you should use the Attach method and then mark the desired field as modified manually.
context.Attach(person);
context.Entry(person).Property(p => p.Name).IsModified = true;
context.SaveChanges();
public async Task<bool> Update(MyObject item)
{
Context.Entry(await Context.MyDbSet.FirstOrDefaultAsync(x => x.Id == item.Id)).CurrentValues.SetValues(item);
return (await Context.SaveChangesAsync()) > 0;
}
It's super simple
using (var dbContext = new DbContextBuilder().BuildDbContext())
{
dbContext.Update(entity);
await dbContext.SaveChangesAsync();
}
Microsoft Docs gives us two approaches.
Recommended HttpPost Edit code: Read and update
This is the same old way we used to do in previous versions of Entity Framework. and this is what Microsoft recommends for us.
Advantages
Prevents overposting
EFs automatic change tracking sets the Modified flag on the fields that are changed by form input.
Alternative HttpPost Edit code: Create and attach
an alternative is to attach an entity created by the model binder to the EF context and mark it as modified.
As mentioned in the other answer the read-first approach requires an extra database read, and can result in more complex code for handling concurrency conflicts.
Assume we have an entity Student and AppDbContext as follows.
class Student
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public int Age { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }
public DbSet<Student> Students { get; set; }
}
Version A
CurrentValues can only work for a tracked entity (found).
Only the changed properties are marked as Modified.
Automatic property mapping that is useful when using type parameter TEntity instead of a fixed type Student.
async Task Edit_A(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Entry(found).CurrentValues.SetValues(incoming);
await db.SaveChangesAsync();
}
}
Version B
It works only on a tracked entity (found).
It is not necessary to map all properties because only the changed properties are marked as Modified.
Manual property mapping so we cannot not use generic type parameter.
async Task Edit_B(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
found.Name = incoming.Name;
found.Age = incoming.Age;
await db.SaveChangesAsync();
}
}
Version C
Update() works only on an untracked entity (incoming) and makes it tracked. Untracking found before invoking Update(incoming) is mandatory because only one entity can be tracked with the given primary key.
All properties (including unchanged ones) are marked as Modified. It is less efficient.
Automatic property mapping that is useful for generic type parameter.
async Task Edit_C(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Update(incoming);
await db.SaveChangesAsync();
}
}
Version D
It is the same as version C. I rewrite again below for the sake of completeness.
It works only on an untracked entity (incoming) and makes it tracked. Untracking found is mandatory because only one entity can be tracked with the given primary key.
All properties (including unchanged ones) are marked as Modified. It is less efficient.
Automatic property mapping that is useful for generic type parameter.
async Task Edit_D(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Entry(incoming).State = EntityState.Modified;
await db.SaveChangesAsync();
}
}
Version E
It works only on an untracked entity (incoming) and makes it tracked. Untracking found is mandatory because only one entity can be tracked with the given primary key.
It is not necessary to map all properties because only properties (including unchanged ones) marked with IsModified=true will be updated. It is less efficient if you mark IsModified=true for unchanged properties.
Manual property mapping so we cannot not use generic type parameter.
async Task Edit_E(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Entry(incoming).Property(s => s.Name).IsModified = true;
db.Students.Entry(incoming).Property(s => s.Age).IsModified = true;
await db.SaveChangesAsync();
}
}
I set it as a Community Wiki, feel free to edit as many as you want.
After going through all the answers I thought i will add two simple options
If you already accessed the record using FirstOrDefault() with tracking enabled (without using .AsNoTracking() function as it will disable tracking) and updated some fields then you can simply call context.SaveChanges()
In other case either you have entity posted to server using HtppPost or you disabled tracking for some reason then you should call context.Update(entityName) before context.SaveChanges()
1st option will only update the fields you changed but 2nd option will update all the fields in the database even though none of the field values were actually updated :)
A more generic approach
To simplify this approach an "id" interface is used
public interface IGuidKey
{
Guid Id { get; set; }
}
The helper method
public static void Modify<T>(this DbSet<T> set, Guid id, Action<T> func)
where T : class, IGuidKey, new()
{
var target = new T
{
Id = id
};
var entry = set.Attach(target);
func(target);
foreach (var property in entry.Properties)
{
var original = property.OriginalValue;
var current = property.CurrentValue;
if (ReferenceEquals(original, current))
{
continue;
}
if (original == null)
{
property.IsModified = true;
continue;
}
var propertyIsModified = !original.Equals(current);
property.IsModified = propertyIsModified;
}
}
Usage
dbContext.Operations.Modify(id, x => { x.Title = "aaa"; });
Personally I see the operation that you are doing is an upsert operation, where if data already exist we update, else we insert. There is one good library from Flexlab to support Upsert operation with this syntax
var country = new Country
{
Name = "Australia",
ISO = "AU",
Created = DateTime.UtcNow,
};
await DataContext.Upsert(country)
.On(c => c.ISO)
.UpdateColumns(c => new Country
{
Name = "Australia"
Updated = DateTime.UtcNow,
})
.RunAsync();
The code will check for the property Country.ISO in the table. If it does not exist yet it will insert new row altogether into the table. Else if any row with the same Country.ISO already exist it will update columns Country.Name and Country.Updated for that row.
This method is very fast because we only do one call to the database instead of two calls to check if the data already exist before we updating or inserting the data.
Note that this answer does not apply to you if your intend is not to do the Upsert operation

Generate and auto increment the Id in Entity Framework database first

I have a table CampaignLanguage. The primary key is Id. It should be auto increment.
So I have the code:
public partial class CampaignLanguage
{
public CampaignLanguage()
{
this.CampaignLanguageQuestions = new HashSet<CampaignLanguageQuestion>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
Then in the controller, I want to save the generated object.
[HttpPost]
public ActionResult Save(int clientId, int campaignId)
{
var campaign = CampaignService.GetCampaignById(campaignId);
var campaignLanguage = campaign.CampaignLanguages.Where(x => x.CampaignId == campaignId).FirstOrDefault();
if (campaignLanguage != null)
{
campaignLanguage.WelcomeMessage = message;
CampaignService.Save(campaignLanguage);
}
else
{
campaignLanguage = new CampaignLanguage();
campaignLanguage.Id = 1;
CampaignService.Save(campaignLanguage);
}
return Redirect("/Campaign/Index/" + clientId);
}
However, I get the error.
{"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries."}
I don't want to change my CampaignService.Save method. So how to fix it?
EDIT
public void Save(CampaignLanguage campaignLanguage)
{
_campaignLanguageRepository.Update(campaignLanguage);
_unitOfWork.Commit();
}
EDIT 1
public virtual void Add(T entity)
{
dbset.Add(entity);
}
public virtual void Update(T entity)
{
dbset.Attach(entity);
dataContext.Entry(entity).State = EntityState.Modified;
}
You should be calling Add instead of Update as this is a new instance you want to insert into the data store. Your Save method should check if the primary (auto incremented key) has a value greater than 0 to see if it is new or not. Alternatively you can also see if it is already attached. There is also no need to call Update, setting the entity to state modified does nothing except ensure that all properties will be written back to the DB but the DbContext implements change tracking on entities so this will already happen (unless you are working in a detached state).
public void Save(CampaignLanguage campaignLanguage)
{
if(campaignLanguage.Id == 0)
_campaignLanguageRepository.Add(campaignLanguage);
else
_campaignLanguageRepository.Update(campaignLanguage);
_unitOfWork.Commit();
}
On a side note: The type DbContext already implements a Unit of Work pattern and DbSet<T> is an implementation of a Repository pattern. There should not be any need to add another customer Unit of work and repository pattern around EF, you are just creating a whole abstraction layer for no reason that will problems with readability as well as issues later when you want to perform more complex operations like joining multiple tables together in a query.
Unfortunately, you need to change your CampaignService.Save. You are trying to update an inexistent campaignLanguage object.
The other problem is you are trying to force a key into an Identity column. You cannot do it with out first set insert_identy to the table.
Maybe, you need to ask for the correct method of CampaignService.

How to get id from Add using UnitOfWork pattern?

I am using the UnitOfWork pattern to abstract database access in my Asp.Net application. Basically I follow the UnitOfWork pattern approach described here:
https://chsakell.com/2015/02/15/asp-net-mvc-solution-architecture-best-practices/
However, I'm struggling to understand, how I will get the Id of a newly added item. Like if I want to add a new customer to my Customer repository, how will I get the customer id? The problem is that Add and Commit are decoupled, and the Id is not known until after Commit.
Is there a way to get the id of an added item, using the UnitOfWork pattern?
My approach is as follows. Simply continue working with the added entity as an object. So instead of returning it's ID, return the added object itself. Then, at some point (typically in the controller) you will call UoW.Commit(); and as a result, the Id property of the added entity will contain the updated value.
At this point, you can start using the Id property and for example store it in a cookie as you said.
Note that I dont want my EF model classes to propagate to my domain layer
I have done a workaround. I think it works pretty well
When you want a repository, for example of DbCars, and you insert a new DomainCar you want to get that Id that was only generated when SaveChanges() is applied.
public DomainCar //domain class used in my business layers
{
public int Id{get;set;}
public string Name{get;set;}
}
public DbCar //Car class to be used in the persistence layer
{
public int Id{get;set;}
public string Name{get;set;}
public DateTime CreatedDate{get;set;}
public string CreatedBy{get;set;}
}
First you create a generic IEntity interface and a class implementing it:
public interface IEntity<T>
{
T Id { get; }
}
public class Entity<T> : IEntity<T>
{
dynamic Item { get; }
string PropertyName { get; }
public Entity(dynamic element,string propertyName)
{
Item = element;
PropertyName = propertyName;
}
public T Id
{
get
{
return (T)Item.GetType().GetProperty(PropertyName).GetValue(Item, null);
}
}
}
Then in your add method of the repository you return a IEntity of the type of your Id:
public IEntity<int> AddCar(DomainCar car)
{
var carDb=Mapper.Map<DbCar>(car);//automapper from DomainCar to Car (EF model class)
var insertedItem=context.CARS.Add(carDb);
return new Entity<int>(insertedItem,nameof(carDb.Id));
}
Then , somewhere you are calling the add method and the consequent Save() in the UnitofWork:
using (var unit = UnitOfWorkFactory.Create())
{
IEntity<int> item =unit.CarsRepository.AddCar(new DomainCar ("Ferrari"));
unit.Save(); //this will call internally to context.SaveChanges()
int newId= item.Id; //you can extract the recently generated Id
}
The problem here is that the id is generated by the database, so we need to call SaveChanges so that the database generates the id and EntityFramework will fix the entity up with the generated id.
So what if we could avoid the database roundtrip?
One way to do this is to use a uuid instead of an integer as an id. This way you could simply generate a new uuid in the constructor of your domain model and you could (pretty) safely assume that it would be unique across the entire database.
Of course choosing between a uuid and an integer for the id is an entire discussion of its own: Advantages and disadvantages of GUID / UUID database keys But at least this is one point in favor of a uuid.
Unit of work should be a transaction for the entire request.
Simply just return the person id from the newly created object.
Depending on what technology you are using for your data access this will differ, but if you are using Entity Framework, you can do the following:
var person = DbContext.Set<Person>().Create();
// Do your property assignment here
DbContext.Set<Person>().Add(person);
return person.Id;
By creating the Person instance this way, you get a tracked instance that allows for lazy loading, and using the Id property, as it will be updated when SaveChanges is called (by ending your unit of work).
Instead of IDENTITY, I use SEQUENCE at database level. When a new entity is being created, first, I get the next value of the sequence and use it as Id.
Database:
CREATE SEQUENCE dbo.TestSequence
START WITH 1
INCREMENT BY 1
CREATE TABLE [dbo].[Test](
[Id] [int] NOT NULL DEFAULT (NEXT VALUE FOR dbo.TestSequence),
[Name] [nvarchar](200) NOT NULL,
CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED ([Id] ASC)
)
C#:
public enum SequenceName
{
TestSequence
}
public interface IUnitOfWork : IDisposable
{
DbSet<TEntity> Set<TEntity>() where TEntity : class;
void Commit(SqlContextInfo contextInfo);
int NextSequenceValue(SequenceName sequenceName);
}
public class UnitOfWork : MyDbContext, IUnitOfWork
{
...
public void Commit(SqlContextInfo contextInfo)
{
using (var scope = Database.BeginTransaction())
{
SaveChanges();
scope.Commit();
}
}
public int NextSequenceValue(SequenceName sequenceName)
{
var result = new SqlParameter("#result", System.Data.SqlDbType.Int)
{
Direction = System.Data.ParameterDirection.Output
};
Database.ExecuteSqlCommand($"SELECT #result = (NEXT VALUE FOR [{sequenceName.ToString()}]);", result);
return (int)result.Value;
}
...
}
internal class TestRepository
{
protected readonly IUnitOfWork UnitOfWork;
private readonly DbSet<Test> _tests;
public TestRepository(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
_tests = UnitOfWork.Set<Test>();
}
public int CreateTestEntity(NewTest test)
{
var newTest = new Test
{
Id = UnitOfWork.NextSequenceValue(SequenceName.TestSequence),
Name = test.Name
};
_tests.Add(newTest);
return newTest.Id;
}
}
I don't think there is a way to do that unless you break the pattern and pass in some extra information about the newly created entity.
Since the Id will only be allocated since commit is successful and if you don't have information about which entities were created/updated/deleted, its almost impossible to know.
I once did it using the code below (I don't recommend it though but I use it for this need specifically)
public string Insert(Person entity)
{
uow.Repository.Add(entity); //public Repository object in unit of work which might be wrong
Response responseObject = uow.Save();
string id = entity.Id; //gives the newly generated Id
return id;
}
My solution is returning Lazy<MyModel> by repository method:
public class MyRepository
{
// ----
public Lazy<MyModel> Insert(MyModel model)
{
MyEntity entity = _mapper.MapModel(model);
_dbContext.Insert(entity);
return Lazy<MyModel>(()=>_mapper.MapEntity(entity));
}
}
And in the domain:
Lazy<MyModel> mayModel = unitOfWork.MyRepository.Insert(model);
unitOfWork.Save();
MyModel myModel = myModel.Value;
To expand on Martin Fletcher his answer:
EF Core generates (depending on the db provider) a temporary value for the generated id, once you add the entity to the DbContext, and start tracking it. So before the unit of work is actually committed via SaveChanges(). After SaveChanges() is called, the DbContext will actually fix up all placeholder ids with the actual generated id.
A nice example (which I quote from this answer):
var x = new MyEntity(); // x.Id = 0
dbContext.Add(x); // x.Id = -2147482624 <-- EF Core generated id
var y = new MyOtherEntity(); // y.Id = 0
dbContext.Add(y); // y.Id = -2147482623 <-- EF Core generated id
y.MyEntityId = x.Id; // y.MyEntityId = -2147482624
dbContext.SaveChangesAsync();
Debug.WriteLine(x.Id); // 1261 <- EF Core replaced temp id with "real" id
Debug.WriteLine(y.MyEntityId); // 1261 <- reference also adjusted by EF Core
More information:
https://learn.microsoft.com/en-us/ef/core/change-tracking/explicit-tracking#generated-key-values
https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations#primary-keys
Another option is to generate the keys on the client side (this is possible with for example UUID-based primary keys). But this has been discussed in other answers.

Using db context and binded object in the same action of a controller. How to Update the db?

I didn't find any relevant answer here so I will trigger you, thanks in advance :
I have a controller with 2 methods of the Edit action, (I simplified it for better understanding):
MrSaleBeta01.Controllers
{
public class PostsController : Controller
{
private MrSaleDB db = new MrSaleDB();
...
// GET: Posts/Edit/5
public ActionResult Edit(int? id)
{
...
}
// POST: Posts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit( Post post, int? CategoryIdLevel1, int? CategoryIdLevel2, int? originalCategoryId)
{
...
Category cnew = db.Categories.Find(post.CategoryId);
MoveFromCategory(post, originalCategoryId);
...
db.Entry(post).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
//move post from his old category (fromCategoryId) to a new one (post.CategoryId):
//returns true on success, false on failure.
public bool MoveFromCategory(Post post, int? fromCategoryId)
{
try
{
if (post.CategoryId == fromCategoryId)
return true;
Category cold = null, cnew = null;
if (fromCategoryId!=null)
cold = db.Categories.Find(fromCategoryId);
if (post.CategoryId != 0)
cnew = db.Categories.Find(post.CategoryId);
if (cold != null)
{
cold.Posts.Remove(post);
}
if( cnew != null)
cnew.Posts.Add(post);
db.Entry(cold).State = EntityState.Modified;
db.Entry(cnew).State = EntityState.Modified;
//db.Entry(p).State = EntityState.Modified;
//db.SaveChanges();
return true;
}
catch (Exception)
{
return false;
//throw;
}
}
}
}
So, the idea is very default: The first method is called by Get and returns the View of Edit. Then I need to save the changes by sending the post object from the view to the HttpPost Edit method.
My Model is something like that (I simplified it for better understanding):
MrSaleBeta01.Models
{
public class Post
{
public int Id { get; set; }
[ForeignKey("Category")]
public virtual int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public Category()
{
this.Categories = new List<Category>();
this.Posts = new List<Post>();
}
#region Primitive Properties
public int CategoryId { get; set; }
public string Name { get; set; }
#endregion
#region Navigation Properties
public virtual IList<Post> Posts { get; set; }
#endregion
}
}
The idea: Every Post needs to have it's Category. Every Category can have multiple Posts or none. (1-N relationship).
The problem:
In the Edit (HttpPost) method, after I update the Category's objects (move the Post from it's category to a different category object. After that I do some other modifications on post object), I get an error in the line of the edit method:
db.Entry(post).State = EntityState.Modified;
saying that:
{"Attaching an entity of type 'MrSaleBeta01.Models.Post' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}
The error is beacuse there is a conflict to the line:
cold.Posts.Remove(post);
And even to the line:
cnew.Posts.Add(post);
I tried to use the solution of AsNoTracking() but without success,
I also tried to change the line "db.Entry(post).State = EntityState.Modified" line to:
db.As.Attach(post)
but that line is even cannot be compiled.
What am I doing wrong? How can I solve that issue?
1) You dont have to call .Attach() nor .State = anything.
You have your Entity created as proxy object (cold = db.Categories.Find(fromCategoryId);), its proxy responsibility to track any changes. As exception say, this COULD be your problem.
2) public int CategoryId { get; set; } should be marked with [Key] (i am not sure if convention mark it as primary key, but i doubt it - i think EF conventions take this PK as FK to Category, which could confuse object graph and behave strangely...)
3) Uh, just noticed... Why are you using your FromCategory method at all? I may overlook something, but looks like it just remove Category from collection and add it to another... EF proxy does this automatically for you, right after post.CategoryId = newCatId;
Edit1:
4) Change public virtual IList<Post> Posts { get; set; } to public virtual ICollection<Post> Posts { get; set; }
Edit2:
1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it?
3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically.
I am not famillier with ASP, i work with desktop MVP/MVVM, so i am not sure here - but from my point of view, you really dont need to touch EntityState as long as you are using var x = db.Set<X>().Create(); (== db.X.Create();) (NOT var x = new X();) for new entities and db.Set<X>().FetchMeWhatever(); (== db.X.FetchMeWhatever();) for everything else (Otherwise you get only POCO without proxy. From your example, it looks like you are doing it right ;) ).
Then you have entity with proxy (thats why you have your reference properties on model virtual - this new emitted proxy type override them) and this proxy will take care for 1:n, m:n, 1:1 relations for you. I think this is why folks are using mappers (not only EF and not only DB mappers) mainly :) For me it looks like, you are trying to do this manually and it is unnecessary and its just making a mess.
Proxy also take care of change tracking (so as i say, you dont need to set EntityState manually, only in extreme cases - I can not think of any right now... Even with concurrency.)
So my advice is:
Use only ICollection<> for referencing collections
Check and get rid of any var entity = new Entity(); (as i say, looks like you are doing this)
Throw away every db.Entry(x).State = EntityState.whatever; (trust EF and his change tracker)
Set only one side of reference - it doesnt matter if Category.Posts or Post.Category or even Post.CategoryId - and let mapper do the work. Please note that this will work only with proxy types (as i say above) on entities with virtual referencing & id & ICollection<> properties.
Btw, there are 2 types of change tracking, snippet and proxy - snippet have original values in RAM and is comparing them at SaveChanges() time, for proxy tracking, you need to have all your properties marked virtual - and comparing them at x.Prop = "x" time. But thats off-topic ;)

How to Validate Business Rule in Service or Repo

I am hoping to create a base Entity Class that includes a validation rule that checks if a field called "Title" is unique (which of course requires a db scan). I want the inherited models to run the validation rule in the repo (or service) layer and send a ValidationResult to the (MVC) client-layer.
The problem is one of inheritance.
public interface IUniqueTitle
{
int Id { get; set; }
string Title { get; set; }
// This is a "multi-client, one database" solution.
// Data is isolated using SiteId
int SiteId { get; set; }
}
// Models such as "MemberClub" and "Assessment" will inherit from this
public class EntityUniqueTitle : IUniqueTitle
{
public int Id { get; set; }
public int SiteId { get; set; }
public string Title { get; set; }
}
// This class will be used in production
public class MemberClub : EntityUniqueTitle
{
}
I wrote an extension method that to check to see if the Title field is unique based on the SiteId
public static bool IsUniqueTitle<T>(this IQueryable<T> items, T currentEntity) where T : IUniqueTitle
{
return items.Where(
item => item.Id != currentEntity.Id // INCASE UPDATING OBJECT
& item.SiteId == currentEntity.SiteId
& item.Title == currentEntity.Title)
.Count() == 0;
}
Here is where I get stuck. Where should I put the validation?
I can put in the Repo but can't figure out how to fire the ValidationResult upon Save
public class RepoUniqueTitle<T> : IRepoUniqueTitle<T> where T : EntityUniqueTitle, new()
{
protected readonly DbContext c;
public Repo(IDbContextFactory f) { c = f.GetContext(); }
public void Insert(T o)
{
if (!c.Set<T>().IsUniqueTitle(o))
{
// ***********************
// PROBLEM HERE, HOW DO I STOP AND SEND A VALIDATIONRESULT TO THE CLIENT?
// IF POSSIBLE, AUOTMATIC WHEN MODEL.ISVALID IS CALLED
// code from base repo class for reference
// if (o is IUniqueTitleForSite)
// IoC.Resolve<IRepoUniqueTitle<T>>().Validate(o);
}
else
c.Set<T>().Add(o);
}
}
I am hoping there is a validation solution that:
Models can inherit from a base Entity
Can make db calls to the inherited entity's collection
Works with ValidationResult so it can be cleanly integrated into MVC Tier
Called during Model.isValid if possible
Note: I am using ProDinner as a basis for an "n-tier code-first EF mvc/wf" solution.
Sorry, a lot of this is new to me. Any help you can provide would be greatly appreciated!
If you have separate business logic layer you should place the validation to that layer. Otherwise why to have that layer if you don't use it to execute business rules?
Anyway unique check is tricky because there is a delay between your query and actual saving of data and another thread can insert the item with the same title during that delay. You should place unique index on Title and SiteId to enforce uniqueness in the database. In such case former problem will result in the exception which you must handle somehow but it is probably better then data duplicity.

Categories

Resources