Access database context from Entity Framework model class - c#

Usually, I create new database entites with dbContext.MyClass.Create(). In my current application, I need to create such an entity from within an extension method of another model class.
I have the two classes DataEntry and WorkSchedule, where one DataEntry can contain multiple WorkSchedules. Therefore, I added a method DataEntry.FillOrUpdateFromWCF(), which calls a web service and updates some fields. So far, so good.
This method also needs to create new WorkSchedules for this same DataEntry in some cases. The problem is, that I, as far as I know, have no reference to the current DataEntry's database context. Sure, I could just create them with new WorkSchedule(), but that would not update them after saving, right?
So is there something like a this.ThisEntitysDatabaseContext.WorkSchedule.Create() method from within the DataEntry class?
public partial class DataEntry {
public async Task FillOrUpdate() {
WcfData[] data = GetSomeDataFromWCF();
foreach(WcfData wd in data) {
WorkSchedule ws = this.PathToContext.WorkSchedule.Create();
ws.Stuff = "test";
this.WorkSchedules.Add(ws);
}
}
}

Sure, I could just create them with new WorkSchedule(), but that would not update them after saving, right?
It would, as long as you attach them to a tracked entity. You really don't want access to the DbContext in an entity class, that's an antipattern. You also should reconsider whether you want your entity classes to contain any logic, but that's up for debate.
So if you have something like this:
public class DataEntry
{
public ICollection<WorkSchedule> Schedules { get; set; }
public void DoWork()
{
Schedules.Add(new WorkSchedule
{
Start = DateTime.Now
});
}
}
Then this will add the proper record and foreign key (assuming that's all set up properly):
using (var db = new YourContext())
{
var dataEntry = db.DataEntries.Single(d => d.Id == 42);
dataEntry.DoWork();
db.SaveChanges();
}

Actually, you don't need to ask the DbContext to Create a DataEntry for you. In fact, this is quite uncommon.
Usually you create the object using new, then fill all the properties, except the primary keys and Add the object to the dbContext
using (var dbContext = new MyDbContext())
{
DataEntry entryToAdd = new DataEntry()
{
// fill the properties you want, leave the primary key zero (default value)
Name = ...
Date = ...
WorkShedules = ...
};
// add the DataEntry to the database and save the changes
dbContext.Add(entryToAdd);
dbContext.SaveChanges();
}
For the WorkSchedules you can use your own function, but you can also assign a value using operator new:
WorkSchedules = new List<WorkSchedule>()
{
new WorkSchedule() {Name = "...", ...},
new WorkSchedule() {Name = "...", ...},
new WorkSchedule() {Name = "...", ...},
},
Note: do not fill the primary key of the work schedule, nor the foreign key of the DataEntry that this Workschedule belongs to, after all, you don't know the value yet.
Entity framework is smart enough to understand the one-to-many relationship, and will add the proper items to the database, with the proper values for the foreign keys.

Related

ef core attach same entity multiple times

I'm sure this was asked before, I don't know what to search for, so it's probably duplicate.
I have code that adds new entity to database. This entity has reference to another entity(Role), and I get it via service. Service creates another instance of dbContext, so I have to attach role to the context after I fetch it. The problem is, when I try to attach two same roles, I get this exception:
'Role' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.'
How should I do it? Code below:
using (var context = new TenantContext(schemaName, connectionString))
{
ApprovalTemplates templates = new ApprovalTemplates();
ApprovalTemplate template = new ApprovalTemplate();
template.Approvers = new List<StageTemplate>();
foreach (var stage in request.Stages)
{
var temp = new StageTemplate();
temp.Order = stage.Order;
temp.Name = stage.Name;
var role = roleService.GetById(stage.RoleId, schemaName);//here I get the role
temp.AvailableActions = new List<ApprovalActionTemplate>();
foreach (var actionId in stage.Actions)
temp.AvailableActions.Add(context.ApprovalActions.First(a => a.Id == actionId));
//when I try to add already attached role, exception is thrown
context.TenantRoles.Attach(role);
temp.Role = role;
template.Approvers.Add(temp);
}
templates.PRApprovalTemplate = template;
context.ApprovalTemplates.Add(templates);
context.SaveChanges();
}
I would share potential approach for this and similar cases with Attach - the rule is very simple, you should never attach Entity with the same Id twice. Good point that there is an easy way to check if it's already attached and if it's attached, you can just use that entity, so best way is to always check local entities before attaching any Entity.
For your case in place of
var role = roleService.GetById(stage.RoleId, schemaName);//here I get the role
it may be:
var localRole = context.Set<TenantRole>().Local.FirstOrDefault(entry => entry.Id.Equals(stage.RoleId));
if (localRole == null)
{
localRole = new TenantRole
{
Id = stage.RoleId,
};
Context.TenantRoles.Attach(localRole);
}
...
temp.Role = localRole;
Because if you know RoleId, you do not need to make DB call just to attach TenantRole to the Context.
Given code works fine, but once someone have many-many places like this, it's becomes to heavy. Potential solution for this would be creating extension method for your Context:
public static class RepositoryExtensions
{
public static T LocalContextEntitiesFinder<T>(this TenantContext context, Guid id) where T : class, ISomeInterfaceThatAllYourDBModelsImplements, new()
{
var localObj = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(id));
if (localObj != null)
{
return localObj;
}
localObj = new T
{
Id = id
};
context.Set<T>().Attach(localObj);
return localObj;
}
}
So you will be able to re-write your code to something like:
...
temp.Role = context.LocalContextEntitiesFinder<TenantRole>(id: stage.RoleId);
...
To make it work, you should add interface ISomeInterfaceThatAllYourDBModelsImplements similar to this (in place of Guid you can use any other type you like):
public interface ISomeInterfaceThatAllYourDBModelsImplements
{
public Guid Id { get; set; }
}
And update TenantRole
public class TenantRole: ISomeInterfaceThatAllYourDBModelsImplements
{
[Key]
public Guid Id { get; set; }
...
I hope this may help somebody.

EF6 entity with DatabaseGeneratedOption.Identity Guid Id force insert my Id value

I am trying to use EF to export/import the existing database of a DbContext. In this context, there are several entities with Guid Id properties with DatabaseGeneratedOption.Identity defined by the ModelBuilder. When I re-import the entities, I want to use the Id value from the serialized object, but it always generates a new Id value when I save the changes. Is there any way to force EF to use my Id value in this case? I know DatabaseGeneratedOption.None will allow me to do it, but then I will always be responsible for generating the Id. I know there segmentation issues of the index that occur without using sequential Guids, so I do not want to do this.
Am I out of luck or has anyone found a trick?
Update: we have decided to simply change all Guid Id from DatabaseGeneratedOption.Identity to DatabaseGenerationOption.None and provide the Id ourselves. Although this leads to index fragmentation, we do not expect this to be a problem with the smaller size of our tables.
You can achieve what you want by defining two contexts that derive from a base context. One context defines its keys with DatabaseGeneratedOption.Identity, the other one with DatabaseGeneratedOption.None. The first one will be your regular application's context.
This is possible by virtue of Guid primary keys not being real identity columns. They're just columns with a default constraint, so they can be inserted without a value, or with a value without having to set identity_insert on.
To demonstrate that this works I used a very simple class:
public class Planet
{
public Guid ID { get; set; }
public string Name { get; set; }
}
The base context:
public abstract class BaseContext : DbContext
{
private readonly DatabaseGeneratedOption _databaseGeneratedOption;
protected BaseContext(string conString, DatabaseGeneratedOption databaseGeneratedOption)
: base(conString)
{
this._databaseGeneratedOption = databaseGeneratedOption;
}
public DbSet<Planet> Planets { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Planet>().HasKey(p => p.ID);
modelBuilder.Entity<Planet>().Property(p => p.ID)
.HasDatabaseGeneratedOption(this._databaseGeneratedOption);
base.OnModelCreating(modelBuilder);
}
}
The context subclasses:
public class GenerateKeyContext : BaseContext
{
public GenerateKeyContext(string conString)
: base(conString, DatabaseGeneratedOption.Identity)
{ }
}
public class InsertKeyContext : BaseContext
{
public InsertKeyContext(string conString)
: base(conString, DatabaseGeneratedOption.None)
{ }
}
I first run some code to create and seed the source database:
var db1 = #"Server=(localDB)\MSSQLLocalDB;Integrated Security=true;Database=GuidGen";
var db2 = #"Server=(localDB)\MSSQLLocalDB;Integrated Security=true;Database=GuidInsert";
// Set initializers:
// 1. just for testing.
Database.SetInitializer(new DropCreateDatabaseAlways<GenerateKeyContext>());
// 2. prevent model check.
Database.SetInitializer<InsertKeyContext>(null);
using (var context = new GenerateKeyContext(db1))
{
var earth = new Planet { Name = "Earth", };
var mars = new Planet { Name = "Mars", };
context.Planets.Add(earth);
context.Planets.Add(mars);
context.SaveChanges();
}
And a target database:
using (var context = new GenerateKeyContext(db2))
{
context.Database.Initialize(true);
}
Finally this is the code that does the actual job:
var planets = new List<UserQuery.Planet>();
using (var context = new GenerateKeyContext(db1))
{
planets = context.Planets.AsNoTracking().ToList();
}
using (var context = new InsertKeyContext(db2))
{
context.Planets.AddRange(planets);
context.SaveChanges();
}
Now in both databases you'll see two records with identical key values.
You might wonder: why can't I use one context class, and construct it either with or without the Identity option? That's because EF builds the EDM model only once for a context type and stores it in the AppDomain. So the option you use first would determine which model EF will use for your context class.

Save a complex entity using EF from JSON

I've been trying to save a complex entity in EF code-first using Json.NET for a couple of days without success.
[Major edit and tl;dr;:] Is there a way to deserialise a JSON object into an entity and keep their relationships?
I can store it the regular way. My problem is after deserialising the object.
By design, the Preferences should added to the database, but their Values are foreign keys (giving a PreferenceValue table).
This is my model (oversimplified for brevity):
public class Preference {
public virtual ICollection<PreferenceAttribute> Attributes { get; set; }
}
public class PreferenceAttribute {
public virtual ICollection<Value> Values { get; set; }
}
public class Value {
public int ValueId { get; set; }
public string Description { get; set; }
}
The values seem not to be attached to the context before saving, causing the engine to store new Values instead of using the foreign keys provided by the JSON object, which looks like:
{
"PreferenceAttributes":[{
"PreferenceTypeId" : 1,
"Values":[
{
"ValueId" : 1
},
{
"ValueId" : 2
},
{
"ValueId" : 3
},
]
}]
}
I can save it whithout any problems directly in C#; the "code" I use to seed the preferences:
var attribute = new PreferenceAttribute {
AttributeId = 1,
Values = context.Values.OrderBy(a => a.ValueId).Skip(1).Take(5).ToList();
};
var preferece = new Preference {
Attributes = new List<PreferenceAttribute> {
attribute
}
};
//user is fetched from "context" as well
user.Preferences.Add(preferece);
context.SaveChanges();
Please, keep in mind that it's just about the Values. The issue is, as noted before, that new Values are being added to the database instead of using their Ids as foreign keys to relate to PrefferenceAttributes, i.e., EF is thinking that I want to add new Values, like so:
attribute.Values = new List<Value> {
new Value {
ValueId = 1, //This id will be ignored by EF since it's not fetched using context; new record will be inserted;
WhateverAttributes = "WTF"
}
}
Best regards.
I think your problem is foreign key records are not referenced by existing record, instead they are newly created and referenced.
I think that issue occurs because all your entities are in Added state. You may want your foreign key records in Unchanged state.
Check this link for entity states http://msdn.microsoft.com/en-us/data/jj592676.aspx
First of all, I would advice you to separate domain model from Dto. Once you do this, you are going to resolve foreign key records manually before you flush them out.

Change id of foreign key object while add new object in EF

I use EF as ORM, but I have problem with simple add object with foreign keys. I execute this code below to add new article to DB.
dataLayer = new CmsDataLayer();
var newArticle = new Article();
newArticle.Author = AuthorService.CurrentAuthor; //id =8
dataLayer.Articles.Insert(newArticle);
There is CmsDataLayer.ICmsRepository is repository pattern (simple CRUD operation)
class CmsDataLayer
{
public ICmsRepository<Author> Authors = new MsSqlServerCmsRepository<Author>();
}
In method insert I do this
class MsSqlServerCmsRepository
{
private DbSet<T> dataSet;
public MsSqlServerCmsRepository()
{
dataSet = dataContext.Set<T>();
}
public void Insert(T entity)
{
this.dataSet.Add(entity);
this.dataContext.SaveChanges(); //<--
}
After this operation newArticle.Author gets new value of Author ID, before SaveChanges() entity it has Author with id=8, after Author has id=14.
I don't understand why EF change AuthorId after save operation, author with id=8 exists w DB?
I don't know which data context the current author is loaded from in the author service, but I think you need to get it from the same context or attach it. Otherwise it will see it as a new object and insert it again. Alternatively, rather than setting the navigation property, you can leave it sert to null and set the key field .AuthorId instead.
Is the author loaded from the same DB context (or attached to it)?
If not set author ID instead of navigation property.
Also save changes isn't supposed to be in the add method, it is supposed called before closing the DB context.
Try to apply this modification:
public void InsertArticle(Article entity)
{
entity.Author = this.dataContext.Authors.Find(entity.AuthorEntityIdPropertyName);
this.dataSet.Add(entity);
this.dataContext.SaveChanges(); //<--
}
Looking your code I think the AuthorService.CurrentAuthor is not tracked by the MsSqlServerCmsRepository's dataContext. So that it will insert it wiht the next incremented id.

Inserting a record that references records from other tables using Entity Framework

This is a fairly basic question, but I was quite surprised not to find a decent answer anywhere.
Consider the following database schema:
Person {PersionID, Name, CountryOfBirthID [references Country]}
Country {CountryID, CountryName}
The application reads all the countries from the database during the startup and keeps them in a list.
Whenever I'm adding a new person, I'm creating a new Country object with ID and data from the list mentioned above. I'm expecting EF to insert a new Person with the ID referencing a record in Country table, but it inserts a new record to Country table and assigns it a new ID, which is obviously not what I want.
What is the conventional way of inserting a new record that references records in other tables?
I assume, I can fetch a Country record by ID whenever I am adding a Persion (under the same context) and reuse a fetched record, but is there some other way that would allow me to access only Person table?
The simple answer is that you need to 'Attach' your Country first (i.e, letting Entity Framwork knows that's it's already exists in the context/database):
var person = new Person { ... };
var country = new Country { Id = ... } // or _countriesList[2] in your case;
person.Country = country;
context.Attach(country);
context.SaveChanges();
Also, if you created your entities using the Designer in Visual Studio, your entities should contain a property named 'EntityReference' which you can use to achieve the same result:
var person = new Person {};
person.Country.EntityReference = new EntityKey("MyEntities.Countries", "CountryId", countryId);
(Replace 'MyEntities.Countries' and 'CountryId' with the appropriate values)
Yet, i believe the easiest way is to 'expose' the foreign key in your entity:
class Person
{
....
Country CountryOfBirth { get; set; }
int CountryOfBirthID { get; set; }
}
//then:
var person = new Person { ... };
person.CountryOfBirthId = 2;
If you're looking for a more generic way to handle this, look for the Identity Map pattern.

Categories