I'm trying to expose static objects using Microsoft.EntityFrameworkCore in a .netcoreapp 2.1.
The data I want to expose is in a .json file, and I can deserialize it without any issue into its corresponding c# classes.
Here's a sample of the structure : https://pastebin.com/SKCKsDJi
For the sake of clarity i suggest you read it using your favourite json reader
And here are the c# version of those objets :
public class FoodItem
{
public int Id { get; set; }
public string Name { get; set; }
public FoodType Type { get; set; }
public string Picture { get; set; }
public float Price { get; set; }
public string Currency { get; set; }
public IEnumerable<Ingredient> Ingredients { get; set; }
public bool IsVegetarian { get; set; }
}
public class FoodType
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Ingredient
{
public int Id { get; set; }
public string Name { get; set; }
}
Lets keep it simple though, there are ingredients, types and items, items are basically sandwiches, which are of a certain type and contain a list of ingredients. All 3 have id's to match them. This is where my problem lies, or so I think.
Everything works fine if I'm just using "Types" for example, in my dbcontext. As soon as I try to add either ingredients or items, or all 3 (which I need, but baby steps), I have the following error.
InvalidOperationException: The instance of entity type 'Ingredient' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
This is caused when :
public EatupController(EatupContext context)
{
_context = context;
var completeModel = JsonConvert.DeserializeObject<EatUpDataModel>(EatUpDataSet.Complete);
_context.Types.AddRange(completeModel.Types); //Works
_context.Ingredients.AddRange(completeModel.Ingredients); //Crashes here.
//If removed, all is fine but data is incomplete
//_context.Types.AddRange(completeModel.Types); //Unused
//_context.Items.AddRange(completeModel.Items); //Unused
_context.SaveChanges();
}
I don't understand why it's complaining about duplicate ID's, because they're all identical. Except, when I'm referencing the ingredient X in an item, obviously some items will use ingredients used by other items (many sandwiches have tomatoes). But surely that type of relation is allowed.
At first I had id's starting at 0 for all different types of objets, so ingredients ranged from 0 to about 100, items from 0 to 60, and types from 0 to 7. But since I had that error I edited all Id's and I still have the error, which is very confusing.
From what I read, this might also be due to using the context in different threads but this is not the case. If I remove the line that crashes, it stops crashing and I can see the data in the context correctly. In this case, only the types. If I add only the items or the ingredients in the context, it crashes for the same reason, just in another object (ingredient or item).
Where should I go from here? I don't even have a bad solution I could try to implement. My worst idea was to manually change the Id's (which is silly to me, it should work with the older ones), but even that failed.
Related
I have a website that is using EF Core 3.1 to access its data. The primary table it uses is [Story] Each user can store some metadata about each story [StoryUserMapping]. What I would like to do is when I read in a Story object, for EF to automatically load in the metadata (if it exists) for that story.
Classes:
public class Story
{
[Key]
public int StoryId { get; set; }
public long Words { get; set; }
...
}
public class StoryUserMapping
{
public string UserId { get; set; }
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
}
public class User
{
[Key]
public string UserId { get; set; }
...
}
StoryUserMapping has composite key ([UserId], [StoryId]).
What I would like to see is:
public class Story
{
[Key]
public int StoryId { get; set; }
public bool ToRead { get; set; } //From user mapping table for currently logged in user
public bool Read { get; set; } //From user mapping table for currently logged in user
public bool WontRead { get; set; } //From user mapping table for currently logged in user
public bool NotInterested { get; set; } //From user mapping table for currently logged in user
public byte Rating { get; set; } //From user mapping table for currently logged in user
...
}
Is there a way to do this in EF Core? My current system is to load the StoryUserMapping object as a property of the Story object, then have Non-Mapped property accessors on the Story object that read into the StoryUserMapping object if it exists. This generally feels like something EF probably handles more elegantly.
Use Cases
Setup: I have 1 million stories, 1000 users, Worst-case scenario I have a StoryUserMapping for each: 1 billion records.
Use case 1: I want to see all of the stories that I (logged in user) have marked as "to read" with more than 100,000 words
Use case 2: I want to see all stories where I have NOT marked them NotInterested or WontRead
I am not concerned with querying multiple StoryUserMappings per story, e.g. I will not be asking the question: What stories have been marked as read by more than n users. I would rather not restrict against this if that changes in future, but if I need to that would be fine.
Create yourself an aggregate view model object that you can use to display the data in your view, similar to what you've ended up with under the Story entity at the moment:
public class UserStoryViewModel
{
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
...
}
This view model is concerned only about aggregating the data to display in the view. This way, you don't need to skew your existing entities to fit how you would display the data elsewhere.
Your database entity models should be as close to "dumb" objects as possible (apart from navigation properties) - they look very sensible as they are the moment.
In this case, remove the unnecessary [NotMapped] properties from your existing Story that you'd added previously.
In your controller/service, you can then query your data as per your use cases you mentioned. Once you've got the results of the query, you can then map your result(s) to your aggregate view model to use in the view.
Here's an example for the use case of getting all Storys for the current user:
public class UserStoryService
{
private readonly YourDbContext _dbContext;
public UserStoryService(YourDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<IEnumerable<UserStoryViewModel>> GetAllForUser(string currentUserId)
{
// at this point you're not executing any queries, you're just creating a query to execute later
var allUserStoriesForUser = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
// calling .ToList()/.ToListAsync() will then execute the query and return the results
return allUserStoriesForUser.ToListAsync();
}
}
You can then create a similar method to get only the current user's Storys that aren't marked NotInterested or WontRead.
It's virtually the same as before, but with the filter in the Where to ensure you don't retrieve the ones that are NotInterested or WontRead:
public Task<IEnumerable<UserStoryViewModel>> GetForUserThatMightRead(string currentUserId)
{
var storiesUserMightRead = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId && !mapping.NotInterested && !mapping.WontRead)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
return storiesUserMightRead.ToListAsync();
}
Then all you will need to do is to update your View's #model to use your new aggregate UserStoryViewModel instead of your entity.
It's always good practice to keep a good level of separation between what is "domain" or database code/entities from what will be used in your view.
I would recommend on having a good read up on this and keep practicing so you can get into the right habits and thinking as you go forward.
NOTE:
Whilst the above suggestions should work absolutely fine (I haven't tested locally, so you may need to improvise/fix, but you get the general gist) - I would also recommend a couple of other things to supplement the approach above.
I would look at introducing a navigation property on the UserStoryMapping entity (unless you already have this in; can't tell from your question's code). This will eliminate the step from above where we're .Selecting into an anonymous object and adding to the query to get the Storys from the database, by the mapping's StoryId. You'd be able to reference the stories belonging to the mapping simply by it being a child navigation property.
Then, you should also be able to look into some kind of mapping library, rather than mapping each individual property yourself for every call. Something like AutoMapper will do the trick (I'm sure other mappers are available). You could set up the mappings to do all the heavy lifting between your database entities and view models. There's a nifty .ProjectTo<T>() which will project your queried results to the desired type using those mappings you've specified.
i have this class which contains a list of the object ConversieDetail
public class ConversieRun
{
[Key]
public String Guid { get; set; }
public String Naam { get; set; }
public String Status { get; set; }
public DateTime Start { get; set; }
public DateTime? Einde { get; set; }
public List<ConversieDetails> Details { get; set; }
}
With the following method i need to return a list of ConversieRun including the ConversieDetails
public List<PGData.ConversieRun> GetAll()
{
//var result = _context.CoversieDetails.ToList();
return _context.ConversieRun.ToList();
}
however when i return with above example the conversieDetail List is null.
now when i uncomment the result list, the List of conversieDetails will be filled in the ConversieRun object as expected.
any reason why the list of ConversieDetails is null if i don't get them first in another list?
thanks in advance.
Relationships in entities are not loaded by default and will be null. You can explicitly tell EF to also load the related entities by using the Include like this:
_context.ConversieRun.Include(x => x.Details).ToList();
Now all ConversieRun entities will be loaded including their details.
You can read more about this in the "Loading Related Data" section of the documentation (https://learn.microsoft.com/en-us/ef/core/querying/related-data)
The example here is using the Explicit loading method, you can also choose to use the Lazy loading method where the related entities are loaded when you request them. This can however have a negative impact on the number of database queries as it would run a separate query for every ConversieRun entity to get its details.
How would you delete a relationship assuming you had the 2 entities, but did not have the 'relationship' entity?
Assuming the following entities...
Model classes:
public class DisplayGroup
{
[Key]
public int GroupId { get; set; }
public string Description { get; set; }
public string Name { get; set; }
public ICollection<LookUpGroupItem> LookUpGroupItems { get; set; }
}
public class DisplayItem
{
[Key]
public int ItemId { get; set; }
public string Description { get; set; }
public string FileType { get; set; }
public string FileName { get; set; }
public ICollection<LookUpGroupItem> LookUpGroupItems { get; set; }
}
public class LookUpGroupItem
{
public int ItemId { get; set; }
public DisplayItem DisplayItem { get; set; }
public int GroupId { get; set; }
public DisplayGroup DisplayGroup { get; set; }
}
Here is the code for deleting a relationship. Note: I do not want to delete the entities, they just no longer share a relationship.
public void RemoveLink(DisplayGroup g, DisplayItem d)
{
_dataContext.Remove(g.LookUpGroupItems.Single(x => x.ItemId == d.ItemId));
}
The method above causes an error:
System.ArgumentNullException occurred
Message=Value cannot be null.
It looks like this is the case because LookUpGroupItems is null, but these were called from the database. I would agree that I do not want to load all entity relationship objects whenever I do a Get from the database, but then, what is the most efficient way to do this?
Additional NOTE: this question is not about an argument null exception. It explicitly states how to delete a relationship in Entity Framework Core.
The following is not the most efficient, but is the most reliable way:
public void RemoveLink(DisplayGroup g, DisplayItem d)
{
var link = _dataContext.Find<LookUpGroupItem>(g.GroupId, d.ItemId); // or (d.ItemId, g.GroupId) depending of how the composite PK is defined
if (link != null)
_dataContext.Remove(link);
}
It's simple and straightforward. Find method is used to locate the entity in the local cache or load it the from the database. If found, the Remove method is used to mark it for deletion (which will be applied when you call SaveChanges).
It's not the most efficient because of the database roundtrip when the entity is not contained in the local cache.
The most efficient is to use "stub" entity (with only FK properties populated):
var link = new LookUpGroupItem { GroupId = g.GroupId, ItemId = d.ItemId };
_dataContext.Remove(link);
This will only issue DELETE SQL command when ApplyChanges is called. However it has the following drawbacks:
(1) If _dataContext already contains (is tracking) a LookUpGroupItem entity with the same PK, the Remove call will throw InvalidOperationException saying something like "The instance of entity type 'LookUpGroupItem' cannot be tracked because another instance with the key value 'GroupId:1, ItemId:1' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached."
(2) If database table does not contain a record with the specified composite PK, the SaveChanges will throw DbUpdateConcurrencyException saying "Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions." (this behavior is actually considered a bug by many people including me, but this is how it is).
Shorty, you can use the optimized method only if you use short lived newly create DbContext just for that operation and you are absolutely sure the record with such PK exists in the database. In all other cases (and in general) you should use the first method.
I am working with Entity Framework 6 and I have an product object that has a list of variants like so:
public class Product
{
public int ProductId { get; set;}
public virtual ICollection<Variant> Variants { get; set;}
... other properties
}
public class Variant
{
public int VariantId { get; set;}
public int ProductId { get; set;}
public virtual Product Product { get; set;}
public int StoreId { get; set;}
... other properties
}
And I use this to get the products from the context:
public static GetProducts()
{
using (MyDBContext context = new MyDBContext())
{
return context.Products.Include(p => p.Variants);
}
}
Now this all works fine and when I get the Product it comes back with the variants. However, this morning I foolishly used the Product from the context instead of a DTO and filtered the variants based on a StoreId and now whenever I get a Product, it only returns the variants for that store (even though I never committed any changes).
I have checked the db and all the variants are still there so how do I reset my context so that I get all variants again.
I have tried the following:
resetting iis
cleaning and rebuilding the solution
changing the object to return an extra property
reloading the product using:
foreach (Product product in context.Products)
{
context.Entry(product).Reload();
}
But nothing seems to work, is there anything else I need to do to reset the context?
As it turns out it was a config error with the variant entity rather than the filtered context. I was only using the VariantID as the Key but as there were multiple variants with the same id, it was overriding the following variants with the same values as the first one with that id when it was mapping the objects back after the query (making it look as if there was only the store I had filtered).
I fixed this by making the key unique to the store and the variant id:
HasKey(v => new {v.VariantId, c.StoreId});
I'm having trouble trying to get ValueInjector to map my objects correctly. This is the code I am using for the mapping:
public IEnumerable<CategoryDTO> FindCategories(IList<object[]> criteria)
{
IEnumerable<Category> categories = _categoryRepo.Find(criteria);
IEnumerable<CategoryDTO> categoriesDto = Mapper.Map<IEnumerable<Category>, IEnumerable<CategoryDTO>>(categories);
return categoriesDto;
}
the variable categories contains a property:
IEnumerable<Standard> Standards
This property contains two Standard objects in the instance I'm calling on. The problem is when I map from my Category to my CategoryDTO. CategoryDTO is defined as this:
public class CategoryDTO : AuditableDTO
{
public Guid CategoryId { get; set; }
public string Name { get; set; }
public string MachineName { get; set; }
public string Description { get; set; }
public IEnumerable<StandardDTO> Standards { get; set; }
}
After the mapping statement is run, and I investigate the contents of categoriesDto.Standards, I can see that it is null. I would have expected my Standards to have mapped, but I'm sure I'm missing something with ValueInjector. Probably something along the lines of telling it how to map Standard to StandardDTO. Any thoughts?
EDIT: I need to clarify, I'm using this http://valueinjecter.codeplex.com/wikipage?title=Automapper%20Simulation&referringTitle=Home
EDIT 2: Digging deeper, I can see that my Iesi.Collections.HashedSet is causing the issue. Categorys' Standards property are typed as Iesi.Collections.ISet, this is turned into the HashedSet. So I guess my real question is how do I check the property for that type and how can I map?
My guess would be that the Mapper.Map doesn't know to map one level deeper than the IEnumerable. Have you tried looping though the collection, mapping it at the Category, CategoryDTO level vs the IEnumerable level?