Disconnected Entities in EF Core - c#

I have been looking to log changes on entities in a disconnected scenario.
Original Problem: I call DbContext.Update(Entity) on an updated complex entity and even though nothing has changed, everything is marked as Changed by the ChangeTracker and my concurrency RowVersion column counts up.
POST User with no changes
"userSubPermissions": [],
"fidAdresseNavigation": {
"idAdresse": 1,
"fidPlzOrt": 3,
"strasse": "Gerold Str.",
"hausnr": "45",
"rowVersion": 10,
"isDeleted": 0,
"fidPlzOrtNavigation": {
"idPlzOrt": 3,
"plz": "52062",
"ort": "Aachen",
"rowVersion": 9,
"isDeleted": 0
}
},
"idUser": 35,
"fidAnrede": null,
"fidAdresse": 1,
"fidAspnetuser": "a7ab78be-859f-4735-acd1-f06cd832be7e",
"vorname": "Max",
"nachmname": "Leckermann",
"eMail": "kunde#leasing.de",
"rowVersion": 11,
"isDeleted": 0
POST Returned user
"userSubPermissions": [],
"fidAdresseNavigation": {
"idAdresse": 1,
"fidPlzOrt": 3,
"strasse": "GeroldPenis Str.",
"hausnr": "45",
"rowVersion": 11,
"isDeleted": 0,
"fidPlzOrtNavigation": {
"idPlzOrt": 3,
"plz": "52062",
"ort": "Aachen",
"rowVersion": 10,
"isDeleted": 0
}
},
"idUser": 35,
"fidAnrede": null,
"fidAdresse": 1,
"fidAspnetuser": "a7ab78be-859f-4735-acd1-f06cd832be7e",
"vorname": "Max",
"nachmname": "Leckermann",
"eMail": "kunde#leasing.de",
"rowVersion": 12,
"isDeleted": 0
The RowVersion logic is in the DBContext and only changes the row version when the Entity State is Modified.
foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
{
var saveEntity = entity.Entity as ISavingChanges;
saveEntity.OnSavingChanges();
}
From my investigation over the past 2 days there seem to be two alternatives for EF Core.
1.- As mentioned on this and many similar posts you can use the TrackGraph https://www.mikesdotnetting.com/article/303/entity-framework-core-trackgraph-for-disconnected-data
Problem: This solution marks the entire entity as changed when the ID of the entity is present even though nothing has changed. I get the exact same result as described above, the RowVersion counts up meaning my Entities an all of its Properties have been marked as Modified.
2.- As mentioned in this post
https://blog.tonysneed.com/2017/10/01/trackable-entities-for-ef-core/ you can download a NuGet package and implement the IMergeable and the ITrackable
interfaces on my Entities.
Problem: The Client needs to track the changes in the Model and pass them to the API, which I want to avoid since Angular does not seem to offer a good solution for this.
3.- There is another solution in the book Programming Entity Framework on how EF 6 handles disconnected scenarios, by recording original values Chapter 4: Recording Original Values.
When an Update comes in, the context reads the entity from the DB and then compares the incoming Entity with the DB Entity to see if any changes have been made on the properties and then marks them as Modified.
https://www.oreilly.com/library/view/programming-entity-framework/9781449331825/ch04.html
Problem: Some of the code described in the book is not implementable in EF Core one example is.
((IObjectContextAdapter)this).ObjectContext
.ObjectMaterialized += (sender, args) =>
{
var entity = args.Entity as IObjectWithState;
if (entity != null)
{
entity.State = State.Unchanged;
entity.OriginalValues =
BuildOriginalValues(this.Entry(entity).OriginalValues);
}
};
There must be a way to implement this clever solution of handling changes on the API instead of the Client in Disconnected scenarios, otherwise, this might be a very useful Feature to have in EF Core since most Developers are using it for Disconnected scenarios.

My response is using TrackGraph
Synopsis
I want to Modify only few columns of the entity as well as Add\Modify nested child entities
Here, I am updating Scenario entity and modifying only ScenarioDate.
In its child entity, i.e. navigation property TempScenario, I am adding a new record
In the nested child entity, Scenariostation, I am adding as well modifying the records
public partial class Scenario
{
public Scenario()
{
InverseTempscenario = new HashSet<Scenario>();
Scenariostation = new HashSet<Scenariostation>();
}
public int Scenarioid { get; set; }
public string Scenarioname { get; set; }
public DateTime? Scenariodate { get; set; }
public int Streetlayerid { get; set; }
public string Scenarionotes { get; set; }
public int? Modifiedbyuserid { get; set; }
public DateTime? Modifieddate { get; set; }
public int? Tempscenarioid { get; set; }
public virtual Scenario Tempscenario { get; set; }
public virtual ICollection<Scenario> InverseTempscenario { get; set; }
public virtual ICollection<Scenariostation> Scenariostation { get; set; }
}
public partial class Scenariostation
{
public Scenariostation()
{
Scenariounit = new HashSet<Scenariounit>();
}
public int Scenariostationid { get; set; }
public int Scenarioid { get; set; }
public int Stationid { get; set; }
public bool? Isapplicable { get; set; }
public int? Createdbyuserid { get; set; }
public int? Modifiedbyuserid { get; set; }
public DateTime? Modifieddate { get; set; }
public virtual Scenario Scenario { get; set; }
public virtual Station Station { get; set; }
}
public partial class Station
{
public Station()
{
Scenariostation = new HashSet<Scenariostation>();
}
public int Stationid { get; set; }
public string Stationname { get; set; }
public string Address { get; set; }
public NpgsqlPoint? Stationlocation { get; set; }
public int? Modifiedbyuserid { get; set; }
public DateTime? Modifieddate { get; set; }
public virtual ICollection<Scenariostation> Scenariostation { get; set; }
}
With EF Core, data update in a disconnected scenario is tricky if you don't want to make 2 database round trips.
Even though 2 database trips seems not significant, it can hamper performance if the data table has millions of records.
Also, if there are only few columns to be updated, including columns of nested child entities, Usual Approach will not work
Usual approach
public virtual void Update(T entity)
{
if (entity == null)
throw new ArgumentNullException("entity");
var returnEntity = _dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
But the problem here for a disconnected EF Core update, if you use this DbContext.Entry(entity).EntityState = EntityState.IsModified, all the columns will updated.
and so some columns will be updated to its default value i.e. null or default data type value.
Further more, some records of ScenarioStation won't be updated at all because the entity state will be UnChanged.
So inorder to update only the columns which are sent from the client, somehow EF Core needs to be told.
Using ChangeTracker.TrackGraph
Recently I found this DbConetxt.ChangeTracker.TrackGraph method which can be used to mark Added, UnChanged state for the entities.
difference is that with TrackGraph, you can add custom logic, as it iteratively navigates through Navigation properties of the entity.
My custom logic using TrackGraph
public virtual void UpdateThroughGraph(T entity, Dictionary<string, List<string>> columnsToBeUpdated)
{
if (entity == null)
throw new ArgumentNullException("entity");
_context.ChangeTracker.TrackGraph(entity, e =>
{
string navigationPropertyName = e.Entry.Entity.GetType().Name;
if (e.Entry.IsKeySet)
{
e.Entry.State = EntityState.Unchanged;
e.Entry.Property("Modifieddate").CurrentValue = DateTime.Now;
if (columnsToBeUpdated.ContainsKey(navigationPropertyName))
{
foreach (var property in e.Entry.Properties)
{
if (columnsToBeUpdated[e.Entry.Entity.GetType().Name].Contains(property.Metadata.Name))
{
property.IsModified = true;
}
}
}
}
else
{
e.Entry.State = EntityState.Added;
}
});
}
With this approach, I am able to easily handle only required column updates as well as new additions/modifications for any of the nested child entities and its columns.

Related

Json into EF with duplicates

I have following sample JSON (simplified):
{
"programmes":[
{
"programmeID":"163",
"title":"Programme 1",
"color":"#ff5f57",
"moderators":[
{
"moderatorID":"27",
"name":"Moderator 1",
}
]
},
{
"programmeID":"153",
"title":"Programme 2",
"color":"#ff5f57",
"moderators":[
{
"moderatorID":"27",
"name":"Moderator 1",
}
]
},
]
}
I want to convert into following objects:
public class Programme
{
[Key]
public string programmeID { get; set; }
public string title { get; set; }
//Navigational Property
public virtual ICollection<Moderator> moderators { get; set; }
}
public class Moderator
{
[Key]
public string moderatorID { get; set; }
public string name { get; set; }
//Navigational Property
public virtual ICollection<Programme> programmes { get; set; }
}
Everything is fine, objects looks great, populated correctly, but when trying to save into db a get this error:
The instance of entity type 'Moderator' cannot be tracked because
another instance with the key value '{moderatorID: 27}' is already
being tracked. When attaching existing entities, ensure that only one
entity instance with a given key value is attached.
This error happens when calling .Add with the list of Programmes:
List<Programme> programmes;
programmes = JsonSerializer.Deserialize<List<Programme>>(JSONcontent);
//... db init
db.AddRange(programmes);
I understand that there is a duplicity for Moderator (ID=27), but I think that it's quite a normal pattern in JSON. I can do some workaround, but wondering if there is any best practice how to handle this in EF directly?
you can not add children you are created from deserialization as an existed ones. One of the solutions is finding the existed children at first
List<Programme> programmes = JsonSerializer.Deserialize<List<Programme>>(JSONcontent);
foreach (var program in programmes)
{
var moderators=new List<Moderator>();
foreach (var moderator in program.moderators)
{
var existingModerator=db.Moderators.FirstOrDefault(i=>
i.moderatorId=moderator.moderatorId);
moderators.Add(existingModerator);
}
programm.moderators=moderators;
}
db.AddRange(programmes);
but I usually prefer to create an explicit class for many to many relations. It will make all your code much more simple.

Entity Tries to Create Child Entity When I just want a Reference

I've been trying to figure out how ASP.net Core with Entity Framework Core works when it comes to child entities. Currently, my issue is that I can create a "SomeThing" entity with the Status as a full entity, inserting both, but I cannot figure out how to create a second "SomeThing" that links to the same Status without it throwing an error saying it already exists?
Below are my models and context code:
Main class:
public class SomeThing
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SomeThingId { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? RequestedDate { get; set; }
[Required(ErrorMessage = "Description is required.")]
public string Description { get; set; }
[Required]
public int CreatedBy { get; set; }
public bool? Archived { get; set; }
//Linked items
[Required(ErrorMessage = "Status is required.")]
[ForeignKey("StatusCode")]
public Status Status { get; set; }
}
Child class:
public class Status
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Code { get; set; }
[Required(ErrorMessage = "The status requires a description")]
public string Description { get; set; }
public Status()
{
}
}
Context:
public class OCWRContext : DbContext
{
public DbSet<Status> Statuses { get; set; }
public DbSet<SomeThing> WorkRequests { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeThing>(entity =>
{
entity.HasKey(st => st.SomeThingId);
entity.Property<int>("SomeThingId")
.ValueGeneratedOnAdd();
entity.Property<byte[]>("Version")
.IsRowVersion();
entity.HasOne(st => st.Status)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Status>(entity =>
{
//entity.HasKey(s => s.Code);
//entity.HasMany(s => s.Code)
//.WithOne();
});
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySQL(connectionString);
}
}
Am I linking it incorrectly? I cannot find much that makes sense to me on this topic. I come from a mainly JS background and have never used EF before so I'm just running in circles at this point.
My current call to create a "SomeThing" is:
POST
{
"CreatedDate": "2018-06-14",
"Description": "There's a thing.",
"CreatedBy": 1,
"Archived": 0,
"Status": {
"Code": "1000",
"Description": "test"
}
}
Any help in getting me going in the correct direction again is greatly appreciated! If you need any other information from me to help please just let me know. I'm dying to figure this out at this point.
Thanks in advance,
Steve
how to create a second "SomeThing" that links to the same Status without it throwing an error saying it already exists
What you're encountering is called Disconnected Entities
...sometimes entities are queried using one context instance and then saved using a different instance.
...In this case, the second context instance needs to know whether the entities are new (should be inserted) or existing (should be updated).
In your case, if the database already has a Status with Code == "A" then you cannot add a new Status with the same Code. What you can do without changing your context or models is something like this:
Find a Status with a given Code
If no Status is found, then create a new Status
Set the Status from step 1 or 2 to the new SomeThing
Sample code for the steps above:
using (var context = new OCWRContext())
{
string statusCode = "A";
Status status =
context.Statuses.Find(statusCode) //Step 1
?? context.Statuses.Add(new Status { Code = statusCode, Description = "" }).Entity; //Step 2
var someThing = new SomeThing
{
Status = status, //Step 3
Description = ""
};
context.Add(someThing);
context.SaveChanges();
}

System.Linq.Dynamic - No property or field exists in type ICollection

I'm using System.Linq.Dynamic with EntityFramework. My entities are below:
public class Customer
{
public Customer()
{
CustomerInterests = new List<CustomerInterest>();
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CustomerInterest> CustomerInterests { get; set; }
}
public class CustomerInterest
{
public int Id { get; set; }
public int CustomerId { get; set; }
public int CourseId { get; set; }
public Course Course { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
}
Below is my method:
public dynamic Get(long customerId)
{
var query = DbContext.Customers.Include("CustomerInterests").Include("CustomerInterests.Course").AsQueryable();
return query.Where(filter => filter.Id == customerId).Select("new(id,name,customerInterests)");
}
JSON result:
{
"id": 2003,
"name": "name customer",
"customerInterests": [
{
"customerId": 2003,
"courseId": 2,
"course": null,
"id": 2016
},
{
"customerId": 2003,
"courseId": 3,
"course": null,
"id": 2017
}
]
}
I'm trying to load the property Course, but it's always returning null as you can see in JSON result.
How can I create the selector new(....) to load correctly the property Course. I've already tried new (customerInterests.course) as customerInterests.course without success.
Do not forget that I am trying to navigate Customer (object) -> CustomerInterests (Collection) -> for each item load Course (object).
I would appreciate if you could help me on this matter.
I had faced that before, and my conclusion is: in Lambda extension methods you can load only 1st level of related object.
You can get list of customerInterests but you can't get foreign records for this list.
But you may use LINQ query instead.
var query=from c in DbContext.Customers
from ci in c.CustomerInterests
from co in ci.Courses
where /// your conditions
select new {
id=c.id,
name=c.name,
customerInterests= new {
customerId= ci.customerId,
courseId=ci.courseId ,
courses= new {
Name=co.Name
/// Other Courses attributes
}
}
}
EDITED
If you're using EF7 , you're able to load second level of foreign records by using ThenInclude method
db.Customers.Include( customer => customer.Orders). ThenInclude( order=> order.OrderDetails);

Many to Many relationship Entity Framework creating new rows

I'm building a feedback functionality. The feedback is supposed to have multiple categories and it should be possible to find feedback based on a category so there is a many to many relationship.
I've set up the following code for this, its designed as code first.
The feeback item:
public class FeedbackItem
{
public FeedbackItem()
{
}
[Key]
public long Id { get; set; }
public virtual ICollection<FeedbackCategory> Categorys { get; set; }
//public
public string Content { get; set; }
public bool Notify { get; set; }
public string SubscriptionUserName { get; set; }
public string SubscriptionUserEmail { get; set; }
public long SubscriptionId { get; set; }
}
The feedback category:
public class FeedbackCategory
{
[Key]
public int Id { get; set; }
public string Value { get; set; }
public virtual ICollection<FeedbackItem> Feedbacks { get; set; }
}
The Database Context:
public class FeedbackContext : DbContext, IFeedbackContext
{
public FeedbackContext() : base("DefaultConnection")
{
//Database.SetInitializer<FeedbackContext>(new FeedbackContextDbInitializer());
}
public DbSet<FeedbackItem> FeedbackItems { get; set; }
public DbSet<FeedbackCategory> Categories { get; set; }
}
And the Initializer
class FeedbackContextDbInitializer : DropCreateDatabaseAlways<FeedbackContext>
{
protected override void Seed(FeedbackContext context)
{
IList<FeedbackCategory> categories = new List<FeedbackCategory>()
{
new FeedbackCategory() { Value = "Android" },
new FeedbackCategory() { Value = "API" }
};
foreach (var feedbackCategory in categories)
{
context.Categories.Add(feedbackCategory);
}
base.Seed(context);
}
}
The code above generates three tables when ran. These being FeedbackCategories, FeedbackCategoryFeedbackItems and FeedbackItems
The table FeedbackCategories is seeded with some already existing categories. The trouble comes when I try to create a new FeedbackItem that has one or more categories.
The Json i provide is the following:
{
"categorys": [
{
"$id": "1",
"Feedbacks": [],
"Id": 1,
"Value": "Android"
}
],
"subscriptionUserName": "name",
"subscriptionUserEmail": "my#email.com",
"content": "this is a feedback item",
"notify": false,
"subscriptionId": 2
}
This is converted into a FeedbackItem and handled by the following code
public class FeedbackSqlRepository : IFeedbackSqlRepository
{
public int Create(FeedbackItem feedback)
{
if (feedback == null)
{
throw new ArgumentNullException("feedback", "FeedbackItem cannot be empty.");
}
using (var context = new FeedbackContext())
{
context.FeedbackItems.Add(feedback);
return context.SaveChanges();
}
}
}
The thing that happens here is that EF creates a new FeedbackItem, a new FeedbackCategory an maps the created feedback item to the newly created feedback category in the FeedbackCategoryFeedbackItems table.
this is not the working i want
I want the following:
Create a new FeedbackItem and reverence an existing FeedbackCategory in the FeedbackCategoryFeedbackItems table. My knowledge of EF is too little to understand what's going wrong here and what to do to get the preferred working.
========
Fixed the issue with the following code inside the Create method from the FeedbackSqlRepository:
foreach (FeedbackCategory feedbackCategory in feedback.Categories)
{
context.Entry(feedbackCategory).State = EntityState.Unchanged;
}
context.FeedbackItems.Add(feedback);
return context.SaveChanges();
Entity Framework will not examine entity contents and determine for you if they are new or added.
DbSet.Add() causes ALL entities in an object graph to be marked as added and to generate inserts when you call SaveChanges().
DbSet.Attach() leaves all entities marked as Unmodified.
If some of your entities are new, some are modified and some are just references then you should use either Add() or Attach() and then manually set entity states where necessary before calling SaveChanges().
DbContext.Entry(entity).State = EntityState.Unmodified

MVC3 EF4 duplicates foreign key object on save

I'm using MVC3 with EF4 code-first. I have the following model:
public class Order {
public int ID { get; set; }
[Required]
public float Price { get; set; }
[Required]
public int PayMethodId { get; set; }
public PayMethod PayMethod { get; set; }
public int? SpecificEventId { get; set; }
public SpecificEvent SpecificEvent { get; set; }
public int? SeasonalTicketId { get; set; }
public SeasonalTicket SeasonalTicket { get; set; }
}
When I try to save an Order object with specificEventId = 2 and specificEvent = X, a new SpecificEvent object is created in the DB, even though there's already a specific event X with ID 2 in the DB. When i try with specificEventId = 2 and specificEvent = null I get a data validation error.
What am I doing wrong? I want SpecificEvent and SeasonalTicket to be nullable, and I don't want EF4 to create a new instance of these objects in the DB whenever I save 'Order'.
Update
This is my code for saving Order in the DB:
public void SaveOrder(Order order)
{
Order fromDb = null;
// If editing an existing object.
if ((fromDb = GetOrder(order.ID)) != null)
{
db = new TicketsDbContext();
db.Entry(order).State = System.Data.EntityState.Modified;
db.SaveChanges();
}
// If adding a new object.
else
{
db.orders.Add(order);
db.SaveChanges();
}
}
When I save, I do reach the else clause.
The real question is, where did you get the instance of X from? It appears as though EF has no knowledge of this instance. You either need to fetch the already existing SpecificEvent through EF and use the proxy it returns to set your navigation property, or else tell EF to "attach" X, so that it knows what your intent is. As far as EF knows, it appears, you are trying to send it a new instance with a conflicting Id, so it is properly issuing the error.

Categories