Entity Framework OnChanging protect primary keys - c#

I have a problem with implementing a check that stops a developer from manually or programmatically updating the primary key in code after the initial creation.
partial class User
{
public User()
{
this.UserId = sGuid.NewSGuid(sGuidType.SequentialAtEnd);
}
partial void OnUserIdChanging(Guid value)
{
//throw if its an edit...
throw new DbEntityValidationException("Cannot mutate primary key");
}
}
This works fine if i'm editing/updating an object, but it won't actually let me create a new Entity in the first place. Is there any way I can check at this point to see if its a new entity or an existing entity?
Thanks in advance,
Pete
UPDATE:
Typical I always find the answer after I post -_- ! Thanks for your answers guy, I'll mark one of yours as a correct answer as they were valid alternatives.
if (this.EntityState != System.Data.EntityState.Detached)
{
throw new DbEntityValidationException("Cannot mutate primary key");
}

This problem is usually solved by making primary key setters private or protected:
public class MyEntity
{
public int Id { get; private set; }
}
If you're using EF designer, just select property in designer and set appropriate access modifier in the property grid - your code generator will do the rest.

Related

EF: manual (non autogenerated) keys require ad hoc handling of new entities

In a MVVM application with EF Core as ORM I decided to model a table with a manually inserted, textual primary key.
This is because in this specific application I'd rather use meaningful keys instead of meaningless integer ids, at least for simple key-value tables like the table of the countries of the world.
I have something like:
Id | Description
-----|--------------------------
USA | United States of America
ITA | Italy
etc. etc.
So the entity is:
public class Country
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
public string Description { get; set; }
}
Here is my viewmodel. It's little more than a container for the ObservableCollection of Countries. Actually it gets loaded from a repository. It's trivial and I inlcuded the entire code at the end. It's not really relevant and I could do with just the DbContext as well. But I wanted to show all the layers to see where the solution belongs to. Oh yes, then it contains the synchronizing code that actually offends EF Core.
public class CountriesViewModel
{
//CountryRepository normally would be injected
public CountryRepository CountryRepository { get; set; } = new CountryRepository(new AppDbContext());
public ObservableCollection<Country> Countries {get; set;}
public CountriesViewModel()
{
Countries = new ObservableCollection<Country>();
Countries.CollectionChanged += Countries_CollectionChanged;
}
private void Countries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (Country c in e.NewItems)
{
CountryRepository.Add(c);
}
}
}
In my MainWindow I just have:
<Window.DataContext>
<local:CountriesViewModel/>
</Window.DataContext>
<DockPanel>
<DataGrid ItemsSource="{Binding Countries}"/>
</DockPanel>
Problem and question
Now this doesn't work. When we try to insert a new record, in this case I do it using the automatic feature of DataGrid I get a:
System.InvalidOperationException: 'Unable to track an entity of type 'Country'
because primary key property 'Id' is null.'
Each time i add a new record to the ObservableCollection I also try to add it back to the repository, that in turn adds it on the EF DbContext that doesn't accept entities with null key.
So what are my options here?
One is postponing the addition of the new record till the Id has been inserted. This is not trivial as the collection handling that I've shown, but this is not the problem. The worst is that this way I would have some record that are tracked by EF (the updated and the deleted and the new with pk assigned) and some that are tracked by the view model (the new ones with the key not yet assigned).
Another is using alternate keys; I would have an integer, autogenerated primary key and the ITA,USA etc code would be an alternate key that would be used also in relations. It's not so bad from as simplicity, but I'd like a application-only solution.
What I'm looking for
I'm looking for a neat solution here, a pattern to be used whenever this problem arises and that plays well in the context of a MVVM/EF application.
Of course I could also look in the direction of the view events, that is force the user to insert the key before of a certain event that triggers the insertion. I would consider it a second-class solution because it is sort of view dependent.
Remaining code
Just for completeness, in case that you want to run the code, here is the remaining code.
DbContext
(Configured for postgres)
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("Host=localhost;Database=WpfApp1;Username=postgres;Password=postgres");
}
public DbSet<Country> Countries { get;set; }
}
Repository
The reason why I implemented the repository for such a simple example is because I think that a possible solution may be to include the new-without-key records managment in the Repository instead of in the viewmodel. I still hope that someone comes out with a simpler solution.
public class CountryRepository
{
private AppDbContext AppDbContext { get; set; }
public CountryRepository(AppDbContext appDbContext) => AppDbContext = appDbContext;
public IEnumerable<Country> All() => AppDbContext.Countries.ToList();
public void Add(Country country) => AppDbContext.Add(country);
//ususally we don't have a save here, it's in a Unit of Work;
//for the example purpose it's ok
public int Save() => AppDbContext.SaveChanges();
}
Probably the cleanest way to address the aforementioned issue in EF Core is to utilize temporary value generation on add. In order to do that, you would need a custom ValueGenerator like this:
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;
public class TemporaryStringValueGenerator : ValueGenerator<string>
{
public override bool GeneratesTemporaryValues => true; // <-- essential
public override string Next(EntityEntry entry) => Guid.NewGuid().ToString();
}
and fluent configuration similar to this:
modelBuilder.Entity<Country>().Property(e => e.Id)
.HasValueGenerator<TemporaryStringValueGenerator>()
.ValueGeneratedOnAdd();
The potential drawbacks are:
In pre EF Core 3.0 the generated temporary value is set onto entity instance, thus would be visible in the UI. This has been fixed in EF Core 3.0, so now Temporary key values are no longer set onto entity instances
Even though the property looks empty (null) and is required (default for primary/alternate keys), if you don't provide explicit value, EF Core will try to issue INSERT command and read the "actual" value back from database similar to identity and other database generated values, which in this case will lead to non user friendly database generated runtime exception. But EF Core in general does not do validations, so this won't be so different - you have to add and validate property required rule in the corresponding layer.

EF 6 calls INSERT instead of UPDATE

This could be a duplicate question but a lot of searching for the words in the title only got me a lot of unrelated results.
I have an entity that's roughly set up like this:
public abstract class A
{
public GUID AId { get; set; }
public string SomeProperty { get; set; }
}
public class B : A
{
public string SomeOtherProperty { get; set; }
}
The context has public DbSet<B> BInstances { get; set; } for B objects. In OnModelCreating, the mapping has A set to ignored and B is mapped to a table called TableB.
The AId field is not auto-generated (not an identity field) but it's set to be primary key, both in the database and in the mapping. In the database, the field is defined as a non-null uniqueidentifier with no default.
At runtime, I'm loading an instance of B using its key (_token is just a CancellationToken):
var b = await (dbCtx.BInstances.FirstOrDefaultAsync(e => e.AId), _token));
Then, a property of b is set and I try to save it back to database:
b.SomeOtherProperty = "some new text";
await (dbCtx.SaveChangesAsync(_token));
At this point, I'm getting a Violation of PRIMARY KEY constraint error from the database, stating that the value of AId cannot be inserted because it'd be a duplicate. Of course, the ID is already in the database, I loaded the entity from there, using the ID. For some reason, EF generates an INSERT statement, not an UPDATE and I don't understand why.
When I check dbCtx.Entry(b).State, it's already set to EntityState.Modified. I'm at a loss - can someone point out what I'm doing wrong? I never had issues with updating entities before but I haven't used EF with GUID primary keys (usually I use long primary keys).
I'm using EF 6 and .NET Framework 4.7.1.
Thank you all for the suggestions - this turned out to be a mapping problem that I caused.
In my OnModelCreating() call, I called MapInheritedProperties() on a type that didn't inherit from a base class (other than object, of course) - this seems to have triggered a problem. Other entities that do share a base class worked fine with the mapping call.
I also called ToTable() directly against the entity class - this broke my table mapping for reasons I do not understand. Once I moved that call inside Map(), it started working as expected.
So I went from this:
entity.ToTable("tablename");
to this:
entity.Map(m => m.ToTable("tablename"));
to solve the problem.
Hopefully this will be useful for future readers.
try this
b.SomeOtherProperty = "some new text";
dbCtx.BInstances.AddOrUpdate(b);
await (dbCtx.SaveChangesAsync(_token));
AddorUpdate will update your b instance if it is already added.

NHibernate: Generate Custom Primary Key in OnPreInsert

In my OData service I have to create a custom primary key in the OnPreInsert event handler.
I know I can't use #event.Id to assign the key because it doesn't expose the setter property.
I used the reflection to set the value of this property as shown below:
public bool OnPreInsert(PreInsertEvent #event)
{
if(#event.Entity is MyEnity)
{
var myEntity = #event.Entity as MyEnity;
string newKey = GetCustomKey(...);
myEntity.myId = newKey;
var property = typeof(AbstractPreDatabaseOperationEvent).GetProperty("Id");
if (property != null)
{
property.SetValue(#event,newKey);
}
}
return false;
}
During the debug mode I can see that the value of #event.Id is initialized properly, however the key saved in the database is not the one I generated in the OnPreInsert event handler.
What am I doing wrong here?
Please, try to check this recent Q&A:
NHibernate IPreUpdateEventListener, IPreInsertEventListener not saving to DB
The point is, that as described here:
NHibernate IPreUpdateEventListener & IPreInsertEventListener
...Here comes the subtlety, however. We cannot just update the entity state. The reason for that is quite simple, the entity state was extracted from the entity and place in the entity state, any change that we make to the entity state would not be reflected in the entity itself. That may cause the database row and the entity instance to go out of sync, and make cause a whole bunch of really nasty problems that you wouldn’t know where to begin debugging.
You have to update both the entity and the entity state in these two event listeners (this is not necessarily the case in other listeners, by the way). Here is a simple example of using these event listeners...
I couldn't find some way to use the reflection to achieve what I described in my question above. I tried to use reflection because I didn't know about the Generators available in NHibernate (as I am new to NHibernate).
I have a table named sys_params which holds the next key values for different tables. My target was to fetch the next key for my table my_entity, assign it to the primary key of the new record, increment the next key value in the sys_params table and save the new record into the database.
To achieve this first I defined following classes.
public class NextIdGenerator : TableGenerator
{
}
public class NextIdGeneratorDef : IGeneratorDef
{
public string Class
{
get { return typeof(NextIdGenerator).AssemblyQualifiedName; }
}
public object Params
{
get { return null; }
}
public Type DefaultReturnType
{
get { return typeof(int); }
}
public bool SupportedAsCollectionElementId
{
get { return true; }
}
}
And then in my mapping class I defined the generator like below:
public class MyEnityMap : ClassMapping<MyEnity>
{
public MyEnityMap()
{
Table("my_entity");
Id(p => p.myId,
m=>{
m.Column("my_id");
m.Generator(new NextIdGeneratorDef(), g =>g.Params( new
{
table = "sys_params",
column = "param_nextvalue",
where = "table_name = 'my_entity'"
}));
});
.......
}
}
Hope this will help someone else. Improvements to this solution are highly appreciated.

object with same key already exists objectstatemanager

I am having an entity which holds the virtual collection of another entity. When i try to insert the data by filling the virtual collection for the newly inserted objects it is throwing the error that object with same key already exists.
I know that when the entity is not created it will have identity field with 0 value. But i need to store the collection of data when i store the data in main table.
public virtual void Insert(TEntity entity)
{
((IObjectState)entity).ObjectState = ObjectState.Added;
entityDbSet.Attach(entity);
dataContext.SyncObjectState(entity);
}
This is the insert method that i am using. and below is the poco classes (partial implementation for extending the classes to hold the collection of data) for this operation.
public partial class UserDefinedData
{
public int ID { get { return this.UserSelectedDValueID; } set { this.UserSelectedDValueID = value; } }
public string Name { get { return this.entityTypeName; } }
public virtual AM_AssetLocations AM_AssetLocations { get; set; }
}
public partial class AM_AssetLocations
{
// Reverse navigation
public virtual ICollection<UserDefinedData> UserDefinedDatas { get; set; }
}
I am passing the data using json. Which is also seems correct. as the virtual collection of data is added to the entity correctly.
{"entity":{"ID":"0","CreatedByID":"0","CreatedDate":"04-13-2014 10:48","ModifiedByID":"","ModifiedDate":"","DeletedByID":"","DeletedDate":"","Deleted":"false","Name":"h","Active":"true","DisplayOrder":"0","Test Decimal":"10","":"","Test Number":"10","Test Plain Text":"h","Test RTF":"<p>hsj</p>","Test Yes No":"false","Test Yes No 2":"true","TestDate":"01-Apr-2014","TestDateTime":"10:00 AM","UserDefinedDatas":[{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"123","ValueNumber":"10"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"124","ValueListItemID":"25"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"122","ValueNumber":"10"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"117","ValueString":"h"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"119","ValueString":"<p>hsj</p>"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"125","ValueYesNo":0},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"126","ValueYesNo":1},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"120","ValueDate":"01-Apr-2014"},{"EntityType":"AM_AssetLocations","EntityTypeID":"0","CreatedByID":"0","UserDefinedFieldID":"121","ValueDate":"08-Apr-2014 10:00 AM"}]}}
Please help me to solve this issue.
Note : Just to solve this same key exception if i try to assign the identity field my self it is throwing referential integrity exception. I know that storing the realative collection should work fine. but it is not working for me. please give me some guidance and solution for this.
Thanks,
sachin
Attach is for attaching existing entities.
Context should assign proper state itself, there's no need to do it manually in your case
public virtual void Insert(TEntity entity)
{
//((IObjectState)entity).ObjectState = ObjectState.Added;
context.TEntityDbSet.Add(entity);//Add, not Attach!
//dataContext.SyncObjectState(entity);
context.SaveChanges()
}
..
http://msdn.microsoft.com/en-us/data/jj592676.aspx
Hi Thanks all for helping out. I changed the way to perform this operation. I have created the stored procedure which accepts the main entity and user defined table type of child collection which can be passed as dataset from .net page. And this works fine for me.
thanks.

Entity Framework - Migrations - Code First - Seeding per Migration

I am looking into Migrations in an effort to clean up our deployment processes. The less manual intervention required when pushing a change to production the better.
I have run into 3 major snags with the migrations system. They are show stoppers if I can not figure out a clean way around them.
1. How do I add Seed data per migration:
I execute the command "add-migration" which scaffolds a new migration file with Up and Down functions. Now, I want to automatically make changes to the data with both Up and Down changes. I don't want to add the Seed data to the Configuration.Seed method as this runs for all migrations which ends in all sorts of duplication problems.
2. If the above is not possible, how do I avoid duplications?
I have an enum that I loop through to add the values to the database.
foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
context.Access.AddOrUpdate(
new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
);
}
context.SaveChanges();
Even though I am using AddOrUpdate, I still get duplicates in the database. The above code brings me to my 3rd and final problem:
3. How can I seed Primary Keys?
My enumerable with the above code is:
public class Access
{
public enum Level
{
None = 10,
Read = 20,
ReadWrite = 30
}
public int AccessId { get; set; }
public string Name { get; set; }
}
I am specifying the values that I want as my primary key, but Entity Framework seems to ignore it. They still end up being 1,2,3. How do I get it to be 10,20,30?
Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?
When I have fixed data that I want to insert with a migration, I put the inserts directly in the Up() migration using calls to Sql("Insert ..."). See the note halfway down this page: how to insert fixed data
You prevent duplicates in the Seed method by calling the AddOrUpdate overload that takes an identifier expression specifying the natural key - see this answer and this blog entry.
Primary keys that are integers are created as identity fields by default. To specify otherwise use the [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute
I think this is a good explanation of Initializer and Seed methods
Here is an example of how to use the AddOrUpdate method:
foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
context.Access.AddOrUpdate(
x => x.Name, //the natural key is "Name"
new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
);
}
As a possible solution to item 1, I made an implementation of the IDatabaseInitializer strategy which will run the Seed method of each pending migration only, you will need to implement a custom IMigrationSeed interface in each of your DbMigration classes, the Seed method will then be implemented right after Up and Down methods of every migration class.
This helps to solve two problems for me:
Group Database Model Migration with Database Data Migration (or Seeding)
Check what part of the Seed migration code should really be running, not checking data in the database but using already known data which is the database model that was just created.
The interface looks like this
public interface IMigrationSeed<TContext>
{
void Seed(TContext context);
}
Below is the new implementation that will call this Seed method
public class CheckAndMigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>
: IDatabaseInitializer<TContext>
where TContext : DbContext
where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
public virtual void InitializeDatabase(TContext context)
{
var migratorBase = ((MigratorBase)new DbMigrator(Activator.CreateInstance<TMigrationsConfiguration>()));
var pendingMigrations = migratorBase.GetPendingMigrations().ToArray();
if (pendingMigrations.Any()) // Is there anything to migrate?
{
// Applying all migrations
migratorBase.Update();
// Here all migrations are applied
foreach (var pendingMigration in pendingMigrations)
{
var migrationName = pendingMigration.Substring(pendingMigration.IndexOf('_') + 1);
var t = typeof(TMigrationsConfiguration).Assembly.GetType(
typeof(TMigrationsConfiguration).Namespace + "." + migrationName);
if (t != null
&& t.GetInterfaces().Any(x => x.IsGenericType
&& x.GetGenericTypeDefinition() == typeof(IMigrationSeed<>)))
{
// Apply migration seed
var seedMigration = (IMigrationSeed<TContext>)Activator.CreateInstance(t);
seedMigration.Seed(context);
context.SaveChanges();
}
}
}
}
}
The good thing here is you have a real EF context to manipulate Seed Data, just like standard EF Seed implementation. However this can get strange if for example you decide to delete a table that was Seeded in a previous migration, you will have to refactor your existing Seed code accordingly.
EDIT:
As an alternative to implement the seed method after the Up and Down, you can create a partial class of the same Migration class, I found this useful as it allows me to safely delete the migration class when I want to re-seed the same migration.
Hi I have found a very useful information for your problem in this link:
Safari Books Online
"1. How do I add Seed data per migration:"
As you see in the example you need to create a new confiugration for seeding.
This seed Configuration must be called after migration.
public sealed class Configuration : DbMigrationsConfiguration
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SafariCodeFirst.SeminarContext context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
"2. If the above is not possible, how do I avoid duplications?"
AddOrUpdate Must help you exactly to avoding the duplicates if you get an error here you might have a configuration error post the call stack please. See the example!
"3. How can I seed Primary Keys?"
Here it is also on your key definition. If your key DatabaseGenerated(DatabaseGeneratedOption.Identity) than you do not have to provide it. In some other senarios you need to create a new one it is depending on the key type.
"Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?"
Not that I know!
OK, so with a bit of bashing I have managed to bash EF into submission.
Here is what I did:
1. There is no way that I found to see data for a specific migration. It all must go into the common Configuration.Seed method.
2. To avoid duplicates I had to do 2 things.
For my enums, I wrote the following seed code:
foreach (var enumValue in Enum.GetValues(typeof(Access.Level)))
{
var id = (int)enumValue;
var val = enumValue.ToString();
if(!context.Access.Any(e => e.AccessId == id))
context.Access.Add(
new Access { AccessId = id, Name = val }
);
}
context.SaveChanges();
So basically, just checking if it exists and adding if not
3. In order for the above to work, you need to be able to insert Primary Key Values. Luckily for me this table will always have the same static data so I could deactivate the auto increment. To do that, the code looks like:
public class Access
{
public enum Level
{
None = 10,
Read = 20,
ReadWrite = 30
}
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int AccessId { get; set; }
public string Name { get; set; }
}

Categories

Resources