NHibernate: Generate Custom Primary Key in OnPreInsert - c#

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.

Related

Primitive Obsession - Strongly typed int ID with auto increment

How to get a strongly typed Id...
public sealed class FileUploadId
{
public int Value { get; }
public FileUploadId(int value)
{
Value = value;
}
}
...which is used within my FileUpload class...
public class FileUpload : EntityBase, IAggregateRoot
{
private FileUpload() { /* Required by EF */ }
public FileUpload(string name, string relativePath, FileUploadType type, string contentType, long? size = null)
{
/* Guard clauses... */
Id = new FileUploadId(0);
/* Other properties... */
}
public FileUploadId Id { get; }
/* Other properties... */
}
...working with identity (int auto increment)?
I tried ValueGeneratedOnAdd()in my TypeConifiguration, but without success...
public class FileUploadTypeConfig : IEntityTypeConfiguration<FileUpload>
{
public void Configure(EntityTypeBuilder<FileUpload> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).HasConversion(x => x.Value, x => new FileUploadId(x)).ValueGeneratedOnAdd();
/* Other configurations... */
}
}
I know there's another option with the HiLo algorithm. But I want to get it work with default int id increment. Is this possible in any way?
I managed to get strongly typed ids with auto incrementing ints working with .net6 and EFCore6 by:
Configuring .HasConversion()
Adding .ValueGeneratedOnAdd()
Adding .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore)
Making sure the strongly typed id is never null
Edit:
There is a pitfall with this approach.
It has to do with the change tracking. Since ef core 3.0 and this GitHub issue, when you are using auto incrementing for your key and the key value is not it's default, the entity is added in the 'Modified' state instead of the 'Added' state. This is a problem for the current solution as we never set null for the strongly typed ids. We would have to manually begin tracking the entity in the 'Added' state (with the DbContext Add method) and other types of automatic tracking will not work (for example adding the entity in a navigational property collection for a one-to-many relationship). Official support for this is being tracked with this GitHub issue.
Honestly, using strongly-typed variables with SQL in EF is a true pain. The best explanation I have seen can be found in this blog post by Andrew Lock.
The core of the problem? Using strongly-typed values can result in conducting the filtering of a query (the WHERE ID = value clause) on the client, requiring the process to retrieve all of the records from the DB to perform the selection locally.
The core to the solution? Using a ValueConverter to cast the strongly-typed value to the correct SQL Server value.
Details are extensive. See the referenced article for detail on how to do it.
I'd advise against letting EF deal with the strongly typed Id. It does not handle it well and there will be cases where it will try to filter things in memory.
But you can have both properties, one for EF queries, and one for everything else.
class FileUpload
{
public int InternalId { get; private set; }
public FileUploadId Id
{
get { return new FileUploadId(InternalId); }
set { InternalId = value.Value; }
}
}
It's leaky, but it works.

EntityFramework update fails randomly on SaveChanges / ASP.NET MVC / postbacks

I'm doing an entity update from a Postback in MVC:
ControlChartPeriod period = _controlChartPeriodService.Get(request.PeriodId);
if (period != null)
{
SerializableStringDictionary dict = new SerializableStringDictionary();
dict.Add("lambda", request.Lambda.ToString());
dict.Add("h", request.h.ToString());
dict.Add("k", request.k.ToString());
period.Parameters = dict.ToXmlString();
// ToDo : fails on save every now and then when one of the values changes; fails on Foreign Key being null
try
{
_controlChartPeriodService.Update(period, request.PeriodId);
return Ok(request);
}
The update method looks like this:
public TObject Update(TObject updated, TKey key)
{
if (updated == null)
return null;
TObject existing = _context.Set<TObject>().Find(key);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(updated);
_context.SaveChanges();
}
return existing;
}
public TObject Get(TKey id)
{
return _context.Set<TObject>().Find(id);
}
The weird thing is first time I run it it usually works fine; if i do a second post back it does not work and fails on an EntityValidation error of a foreign key; however examining the entity the foreign key looks fine and is untouched.
Do I need to somehow synchronize the Context somewhere?
I've been trying to find the differences in when it succeeds or when it does not succeed.
I'm using Injection for the repositories:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped((_) => new DataContext(ConfigSettings.Database.ConnectionString));
services.AddScoped<ControlChartPeriodService, ControlChartPeriodService>();
}
-- update:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public virtual ICollection<ControlChartPoint> ControlChartPoints { get; set; }
[Required]
public virtual ControlChart ControlChart { get; set; }
public string Parameters { get; set; }
In the ControlChartMap we have the following:
HasMany(x => x.Periods)
.WithRequired(c => c.ControlChart);
If you are expecting that the loading of the TObject includes the relationship as well, seems to me your problem is in this line:
TObject existing = _context.Set<TObject>().Find(key);
Find will only load the TObject record but not its relationship(s) , and as there is no explicit reference in code to the ControlChart property it doesn't get lazy loaded - which seems to be the behavior that you are expecting , so I'm assuming you have lazy-loading enabled
The fact that the relationship has been configured as
HasMany(x => x.Periods).WithRequired(c => c.ControlChart);
indicates that TObject's ControlChart property is meant to be a related set of Periods but that doesn't mean that it will force the lazy-load of the relationship when loading a TObject.
(I personally try to avoid the reliance on lazy loading due to things like this, and prefer to actually disable it and to eagerly load the relationships via Include, or alternatively in a less measure, an explicit load , by using Load. It may be a matter of personal taste but I've found this way saves you from many headaches)
So If you have lazy loading enabled and like the way it works, make a reference to the ControlChart property at any point between the load and before saving, like
var chart= existing.ControlChart;
or something similar
Or for non-lazy-loading scenarios, although it can be used in any case:
TObject existing = _context.Set<TObject>().Where(x=>x.Id==key).Include(x=> x.ControlChart).FirstOrDefault();
Have you looked at the different object graphs you try to inject. The generic method could deal on a first pass but could fail on update. Simply also because the resulting SQL or Datastore engine language used does not result in the same upon Insert and Update ...
I exp. this and in my case it came from the Model behind. Look at the Foreign Ks constraints etc ... Some might have not be created properly hence the update failure after ...
This is just a stab in the dark but...
In my experience, if Entity Framework is randomly working/not working, it's because I needed to use async/await to ensure data is successfully collected before the method gets to work.
This would perhaps explain why you're getting a null Foreign Key error. If the method gets ahead of itself, it's going to be attempting to work on a new TObject, rather than the one you want to collect from the database.
Try turning it into an async function and turn all the get/set methods into their Async versions.
i.e.
public async Task<TObject> Update(TObject updated, TKey key)
{
if (updated == null)
return null;
TObject existing = await _context.Set<TObject>().FindAsync(key);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(updated);
_ = await_context.SaveChangesAsync();
}
return existing;
}
Then in the main method (which should also be async)
try
{
_ = await _controlChartPeriodService.Update(period, request.PeriodId);
return Ok(request);
}

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.

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 OnChanging protect primary keys

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.

Categories

Resources