Is there a way to fill a dictionary property with Entity Framework Core?
For performance reasons, we like to search in the application instead of the database. As a list won't scale well, we like to use a dictionary.
For example (simplified example)
class Course
{
public Dictionary<string, Person> Persons { get; set; }
public int Id { get; set; }
}
class Person
{
public string Firstname { get; set; }
public string Lastname { get; set; }
}
Things I tried
System.InvalidOperationException: The property 'Persons' could not be mapped, because it is of type 'Dictionary' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Try adding a value conversion (with HasConversion
), but conversion one only works on a single item and not on collections. The HasMany
already gives a compile error:
builder .HasMany<Person>(c => c.Persons) //won't compile, Persons isn't a IEnumerable<Person> .WithOne().HasForeignKey("PersonId");
Creating a custom collection class (inherited from Collection<T>
and implement InsertItem
, SetItem
etc.) – unfortunately this also won't work because EF Core will add the item to the collection and first after that will fill the properties (at least with our OwnsOne properties, that is not in the demo case) - SetItem
won't be called afterwards.
Adding a "computed" property that will build the dictionary, the setter won't be called (the list is updated every time with partly values, a bit the same as above). See try:
class Course { private Dictionary<string, Person> _personsDict; public List<Person> Persons { get => _personsDict.Values.ToList(); set => _personsDict = value.ToDictionary(p => p.Firstname, p => p); //never called } public int Id { get; set; } }
Of course I could build a dictionary in the Repository (using the Repository pattern ), but that's tricky as I could forget some parts – and I really prefer compile time errors over run-time errors and declarative style over imperative style code.
Update, to be clear
List
instead of Dictionary, the mapping worksI don't think saving a dictionary is a good idea (I can't even image how it would be done in the database). As I can see from you source code you are using the FirstName as key. In my opinion you should change the dictionary to a HashSet. This way you can keep the speed but also save it to the database. Here is an example:
class Course
{
public Course() {
this.People = new HashSet<Person>();
}
public ISet<Person> People { get; set; }
public int Id { get; set; }
}
After this you can create a dictionary from it, or keep using the hashset. Sample for dictionary:
private Dictionary<string, Person> peopleDictionary = null;
public Dictionary<string, Person> PeopleDictionary {
get {
if (this.peopleDictionary == null) {
this.peopleDictionary = this.People.ToDictionary(_ => _.FirstName, _ => _);
}
return this.peopleDictionary;
}
}
Please note that this would mean that your People Set becomes unsynced after you add/remove to/from the dictionary. In order to have the changes in sync you should overwrite the SaveChanges method in your context, like this:
public override int SaveChanges() {
this.SyncPeople();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
this.SyncPeople();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SyncPeople() {
foreach(var entry in this.ChangeTracker.Entries().Where(_ = >_.State == EntityState.Added || _.State == EntityState.Modified)) {
if (entry.Entity is Course course) {
course.People = course.PeopleDictionary.Values.ToHashSet();
}
}
}
EDIT: In order to have a running code, you will need to tell the EF not to map the dictionary, via the NotMapped Attribute.
[NotMapped]
public Dictionary<string, Person> PeopleDictionary { ... }
Seems someone has been struggling with that and found solution. See: Store a Dictionary as a JSON string using EF Core 2.1
The definition of the entity is as follows:
public class PublishSource
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}
In the OnModelCreating method of the database context I just call HasConversion, which does the serialization and deserialization of the dictionary:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PublishSource>()
.Property(b => b.Properties)
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
}
One important thing I have noticed, however, is that when updating the entity and changing items in the dictionary, the EF change tracking does not pick up on the fact that the dictionary was updated, so you will need to explicitly call the Update method on the DbSet<> to set the entity to modified in the change tracker.
You would end up with something like:
private class PersonsDictionary
{
private delegate Person PersonAddedDelegate;
private event PersonAddedDelegate PersonAddedEvent; // there can be other events needed too, eg PersonDictionarySetEvent
private Dictionary<string, Person> dict = ...
...
public void Add(string key, Person value)
{
if(dict.ContainsKey(key))
{
// .. do custom logic, if updating/replacing make sure to either update the fields or remove/re-add the item so the one in the list has the current values
} else {
dict.Add(key, value);
PersonAddedEvent?.(value); // your partial class that holds this object can hook into this event to add it its list
}
}
// ... add other methods and logic
}
public partial class Person
{
[NotMapped]
private Dictionary<string, Person> _personsDict
[NotMapped]
public PersonsDictionary<string, Person> PersonsDict
{
get
{
if(_personsDict == null) _personsDict = Persons.ToDictionary(x => x.FirstName, x => x); // or call method that takes list of Persons
return _personsDict;
}
set
{
// delete all from Persons
// add all to Persons from dictionary
}
}
}
public List<Person> Persons; // possibly auto-generated by entity framework and located in another .cs file
You could add a new property PersonsJson
for storing the JSON data. It automatically serializes or deserializes the JSON data into the Persons
property when data is retrieved from DB or stored to DB. Persons
property is not mapped, only PersonsJson
is mapped.
class Course
{
[NotMapped]
public Dictionary<string, Person> Persons { get; set; }
public string PersonsJson
{
get => JsonConvert.SerializeObject(Persons);
set => Persons = JsonConvert.DeserializeObject<Dictionary<string, Person>>(value);
}
public int Id { get; set; }
}
i don't know if this would solve the problem or not, but when i tried to run your provided code. it triggered a runtime error that required me to modify the Persons
property declaration to like like this
public Dictionary<string, Person> Persons { get; set; } = new Dictionary<string, Person>();
this eliminated the runtime error and every thing went fine.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.