How to tell if a property of an EntityObject is a primary key or foreign key? - sql

Suppose that I have a class generated by Entity Framework called Student.
Student has the following properties:
Id int,
Name, string
Age, int
TeacherId int
Suppose further that Id refers to the primary key in SQL that identifies what student a Student object refers to and TeacherId is a foreign key that tells who the student's teacher is.
Suppose I want to write a function which takes any EntityObject (such as this one) as a parameter and returns information about which properties are primary keys and foreign keys.
How can I do this?
If this is not appropriate, then how can Entity Framework tell me which properties are primary and foreign keys?
For now, let's not take into consideration composite key fields.

Looking over the code that is autogenerated, I can see that primitive properties in the generated class have several attributes, and among these is an EdmScalarPropertyAttribute which has a boolean EntityKeyProperty which seems to indicate whether or not the property is a key.
How to read the values of the attributes is described in an article here: http://msdn.microsoft.com/en-us/library/71s1zwct.aspx
I'll bet that I can find a consistent pattern for how foreign keys are handled as well!

I have little support section in Repository for such questions.
See the GetEntityKeyFields method below
I dont have check routine already written FK, but if you check the entityField details, it will be there as well.
public DbEntityEntry<T> Entry(T entity) { return Context.Entry(entity); }
public DbSet<T> EntityDbSet() { return Context.Set<T>(); } // cant be in core interface since it is EF types
public ObjectContext ObjectContext { get { return ((IObjectContextAdapter) this.Context).ObjectContext; } }
// cant be in core interface since it is EF types
public BosBaseDbContext Context { get; protected set; }
public EntityState GetEntityState(T entity) { return Context.Entry(entity).State; }
public ObjectSet<T> GetObjectSet() { return ObjectContext.CreateObjectSet<T>(); }
public string[] GetEntityFields() { return GetObjectSet().EntitySet.ElementType.Properties.Select(e => e.Name).ToArray(); }
public string[] GetEntityKeyFields() { return GetObjectSet().EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); }
public EntityKey GetEntityKey(T entity) {
if (entity == null) {
return null;
}
return ObjectContext.CreateEntityKey(GetObjectSet().EntitySet.Name, entity);
}

Related

What is the correct way to find and set the primary key generically with EF Core

I am trying to access the primary key from a generic parameter on my entity class which is what all of my DB object inhert from.
The following code works. But is it bad practise to access the DBcontext from within the Entity class? If so, how else can I do this? Is there something like IObjectContextAdapter from EF6?
public abstract class Entity
{
private DBContext _context = new DBContext(null);
[NotMapped]
public int ID
{
get
{
return _context.PrimaryKeyValueInt(this);
}
set
{
_context.PrimaryKeyProperty(this).PropertyInfo.SetValue(this, value);
}
}
}
So, I assume you have a bunch of entity classes with various names for their primary keys, like DoodadId, WidgetId, etc. and want them all to extend a base class with a single property to access the primary key? If so, I've done something like that with Attributes and Reflection. Create an Attribute to use to identify Primary Key properties:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class EntityIdAttribute : Attribute
{
}
Then create the base class and add the property to it. In my case I use Guid for primary keys:
public abstract class Entity
{
[NotMapped]
public virtual Guid EntityId
{
get
{
return (Guid)EntityIdProperty.GetValue(this);
}
set
{
EntityIdProperty.SetValue(this, value);
}
}
private PropertyInfo EntityIdProperty => this.GetType().GetProperties()
.Where(prop => Attribute.IsDefined(prop, typeof(EntityIdAttribute)))
.FirstOrDefault();
}
Then in your entity classes, extend Entity and decorate the primary key property with [EntityId]:
public class Widget : Entity
{
[EntityId]
public Guid WidgetId { get; set; }
// ......
}

Entity Framework Core: using navigation properties without foreign key

I have following object model:
public class SharingRelation:BaseEntity
{
public Guid? Code { get; set; }
public string Value { get; set; }
}
public class SecondLevelShareEntity : BaseEntity
{
public string Name { get; set; }
public Guid? SharingCode { get; set; }
public List<SharingRelation> SharingRelations { get; set; }
}
In my database (it may be poor db design but I need to answer this question for research), SharingRelation is some sort of dependent entity of SecondLevelShareEntity on Code == SharingCode values. I can have two entities of type SecondLevelShareEntity with same SharingCode value. So, for each of them I need to get all related SharingRelation objects depending on Code and SharingCode values. I can do it using SQL and join on this columns. But how can I do it using EF Core and navigation properties (I want to get all dependent entities using Include() for example)? When I configure my entities like this
public class SharingRelationEntityTypeConfiguration : BaseEntityTypeConfiguration<SharingRelation>
{
public override void Configure(EntityTypeBuilder<SharingRelation> builder)
{
base.Configure(builder);
builder.HasOne<SecondLevelShareEntity>().WithMany(x => x.SharingRelations).HasForeignKey(x => x.Code)
.HasPrincipalKey(x => x.SharingCode);
}
}
EF Core creates foreign key and marks it unique. I am obviously getting an error that that is impossible to have several SecondLevelShareEntity with the same SharingCode
System.InvalidOperationException : The instance of entity type 'SecondLevelShareEntity' cannot be tracked because another instance with the key value '{SharingCode: 8a4da9b3-4b8e-4c91-b0e3-e9135adb9c66}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
How can I avoid creation of foreign key, but keep using navigation properties (as far, as I see normal queries with navigations generate simple JOIN statements)
UPDATED I can provide real data in database. SecondLevelShareEntity table looks like this:
_id Name SharingCode
----------------------------------------------------------------------
1 "firstSecondLevelEnt" "efcb1c96-0ef1-4bb3-a952-4a6511ab448b"
2 "secondSecondLevelEnt" "efcb1c96-0ef1-4bb3-a952-4a6511ab448b"
And SharingRelation table looks like this:
_id Value Code
----------------------------------------------------------------------
1 "firstSharingRelation" "efcb1c96-0ef1-4bb3-a952-4a6511ab448b"
2 "secondSharingRelation" "efcb1c96-0ef1-4bb3-a952-4a6511ab448b"

BlToolkit insert data failure in BaseRepository class

I'm using BaseRepository in asp .Net MVC project. Edit operation works but in Add operation, I should make a trick to make it work. In detail, my base repository and BaseEntity classes:
public class BaseRepository<TEntity, T> : IRepository<TEntity, T> where TEntity : BaseEntity<T>
{
private DbManager _context;
private Table<TEntity> Table
{
get { return _context.GetTable<TEntity>(); }
}
public BaseRepository(DbManager context)
{
_context = context;
}
//...
public TEntity Add(TEntity entity)
{
//...
return entity;
}
public TEntity Edit(TEntity entity)
{
_context.Update(entity);
return entity;
}
//...
}
public class BaseEntity<T>
{
[PrimaryKey]
public T Id { get; set; }
}
I tried three ways for Add operation to make it work. First two ways gave errors.
First way(Doesn't work):
public TEntity Add(TEntity entity)
{
_context.Insert(entity);
return entity;
}
Error Message:
Cannot insert explicit value for identity column in table '...' when IDENTITY_INSERT is set to OFF.
--
Second way(Doesn't work):
public TEntity Add(TEntity entity)
{
Table.Insert(() => entity);
return entity;
}
Error Message:
Operation is not valid due to the current state of the object.
--
Third way(Working):
public TEntity Add(TEntity entity)
{
var l = new List<TEntity> { entity };
_context.InsertBatch(l);
return entity;
}
--
Edit operation works without error, but for Add operation I need to make some trick. What is the problem with normal Add operation and, is there a way to make it work?
I tried advice of #Mladen Macanović and I added Identity attribute to primary key in base BaseEntity class, then errors shown above gone for entities having int type primary key.
Errors shown above gone for entities having int type of primary key:
public class BaseEntity<T>
{
[PrimaryKey Identity]
public T Id { get; set; }
}
But, this is not a full solution because, some of my entities have primary key in type of Guid, so adding Identity attribute to them gives another error.
InsertBatch method works without using Identity attribute. So, you can add data without using Identity attribute in BaseEntity class for Identity column. What is the difference of insertbatch method? How can I resolve errors shown above without using InsertBatch method?
The problem is with your database table. That is why you're getting the IDENTITY_INSERT error. Go to the SQL Server Management Studio, right click on the table, Design, and for the primary key column set the property Identity Specification -> (Is identity) to Yes.

NHibernate, a different object with the same identifier value was already associated with the session

I have been working with NHibernate, using Fluent NHibernate for mapping. I solved a lot of issues, and started to think myself as experienced in nhibernate.
However, this error is quite strange.
This is my model:
public class MessageNew
{
public virtual int Id { get; set; }
public virtual string Content { get; set; }
public virtual string Subject { get; set; }
public virtual User User { get; set; }
public virtual bool IsSent { get; set; }
public virtual string AmazonMessageId { get; set; }
}
And my mapping
public class MessageNewMap : ClassMap<MessageNew>
{
public MessageNewMap()
{
Id(x => x.Id);
Map(x => x.Content).CustomSqlType("text");
Map(x => x.Subject);
Map(x => x.AmazonMessageId);
Map(x => x.IsSent);
References(x => x.User);
}
}
Here where exception occurs:
foreach (var userToSend in usersToSend)
{
string body = MailHelper.BuildSomeBody()
if (userToSend != CurrentUser)
{
MessageNew message = new MessageNew
{
User = userToSend,
IsSent = false,
Content = body,
Subject = subject
};
session.Save(message); // Exception thrown
}
}
The exception details:
NHibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: 1779, of entity: Models.MessageNew
at NHibernate.Engine.StatefulPersistenceContext.CheckUniqueness(EntityKey key, Object obj)
at NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(Object entity, EntityKey key, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.FireSave(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.Save(Object obj)
Id generator is database driven auto-increment id generator. (not hilo or any other). NHibernate version is 3.2.0 .
I have tried overloading Equals and GetHashCode, no luck.
The UnitOfWork pattern I am using requires not to commit transaction or flush session inside foreach loop. NHibernate says there is another object with same id, but all i am doing is inserting a new object, which does not have any identifier at all.
I am using the same structure all over my project, and it works well everywhere but this. I am suspicious that it might be because of "Content" property, which is text and set to a large string.
What am i missing here? Or NHibernate is missing something?
Sometimes it happend when we assign the object to the same new object. So first check your model and viewmodel that they are not same.
I had similar problem. I went through a lot of discussions, tutorials and forums, but after writing some unit tests, I realized:
1) session.Contains method works with instances
2)session.Save/SaveorUpdate works with ID
This error shows you have another instances of object with same ID in session.So, contains return false because you are working on different instances and Save/SaveorUpdate throws an exception because there is another object with same ID in session.
I've solved my problem like this(my problem was in Job Entity):
Job lJob = lSession.Load<Job>(this.ID);
if(lJob.ID==this.ID)
lSession.Evict(lJob);
lSession.SaveOrUpdate(this);
I hope it helps you
You can use Evict() to evict an object from a session and then you can do whatever you want.
This error occurs when you have the same object in another session.
messagenew should implement Equals and GetHashCode
public class MessageNew
{
public virtual int Id { get; set; }
public override bool Equals(object obj)
{
var other = obj as MessageNew;
return (other != null) && (IsTransient ? ReferenceEquals(this, other) : Id == other.Id;
}
private int? _cachedHashcode; // because Hashcode should not change
public override int GetHashCode()
{
if (_cachedHashcode == null)
_cachedHashcode = IsTransient ? base.GetHashCode() : Id.GetHashCode();
return _cachedHashcode.Value;
}
public bool IsTransient { get { return Id == 0; } }
}
I read some NH code. It basically inserts the new instance into the database to get its id. Then it checks if the id generated by the database is actually unique. If not, you get this exception.
Your database is not generating unique ids. You most probably forgot to set it to an IDENTITY column.
OR the identity starts counting on 0 instead of 1.
That exception usually indicates that you have 2 separate instances of an object with the same identifier value which you are trying to manage over the same session.
You already have another instance of the entity with that id.
Two possible issues:
1 - Your comparison of the entity does not work. You could override equals as suggested or you could change your test case that you use prior to the save:
if (userToSend.Id != CurrentUser.Id)
2 - You are not generating a unique Id for your entity, you need to either assign an Id yourself, have NHibernate generate one or have your sql server do it for you. In your mapping it is implied that an Identity should be used (Fluents default) but have you set up the column in your database to be and Identity column?
My take: you are not declaring an Id generator. Therefore, as soon as you get two MessageNew instances in the session, they'll both have 0 as the Id.
maybe a bit late but hope this helps.
I had a similar problem when i was trying to save multiple instances of an object over the same session with an auto generated column on them. My solution was giving a diferent value and assign it mannually for each entity, so nhibernates doesn't recognize it as the same primary key for that entity.
[..]
};
session.Clear();
session.Save(message);
Try this, helped me.
Add below two lines before Session.Save or Session.SaveOrUpdate
Session.Clear();
Session.Flush();
This will clear all cached entities with the Session.

Setting CustomSqlType on References

I have a situation where my primary key is a char(2) in SqlServer 2008, and I want to reference it in a one-to-many relationship, but the ManyToOneBuilder (which is returned by ClassMap<>.References()) doesn't have a CustomSqlType() method. Specifically:
public class State
{
// state FIPS code is 2 characters
public virtual string StateCode { get; set; }
public virtual ICollection<County> { get; set; }
}
public class County
{
// state-county FIPS code is 5 characters
public virtual string StateCountyCode { get; set; }
public virtual State State { get; set; }
}
public class StateMap : ClassMap<State>
{
public StateMap()
{
Id(e => e.StateCode).CustomSqlType("char(2)").GeneratedBy.Assigned();
}
}
public class CountyMap : ClassMap<County>
{
public CountyMap()
{
Id(e => e.StateCountyCode).CustomSqlType("char(5)").GeneratedBy.Assigned();
References(e => e.State, "StateCode")
// Here's what I want to do, but can't because the method is not
// implemented on the class ManyToOneBuilder:
.CustomSqlType("char(2)");
}
}
Is there any way to accomplish this without modifying the ManyToOneBuilder? Is there a way to automatically map the FK (i.e. County.StateCode) to the correct type? It's trivial to add CustomSqlType to ManyToOneBuilder, but is that the right thing to do?
Keep your mapping definition as is, add your "State" column definition with
.CustomSqlType("char(2)")
and set for this column Insert=false and update=false.
I've the same problem and in AutoMapping I use this code:
mapping.Map(x => x.IdUniArticolo)
.CustomSqlType("varchar(50)")
.Not.Insert().Not.Update();
mapping.References(x => x.Articolo)
.Column("IdUniArticolo").PropertyRef("IdUniArticolo");
Keep in mind that if NHibernate itself doesn't support it, then Fluent NHibernate can't, and I don't NHibernate supports the scenario you have. I had a similar problem in that I had a 2 column composite key on a table and on one of the fields, I wanted to use an enumerated type which had a custom IUserType to translate it to its appropriate code value in the DB. Couldn't do it, so I was stuck keeping the property of the string type rather than the enumerated type.