简体   繁体   中英

EF Core: use a dictionary property

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

  • Naively just add a dictionary property. This will result the in following error:

    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

  • this isn't a code first approach
  • the idea to change the mapping in EF Core, so no database changes. - I haven't tagged the database on purpose ;)
  • If I use a List instead of Dictionary, the mapping works
  • It's a 1:n or n:m relationship in the database (see HasMany - WithOne)

I 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.

  1. Create a partial class of the type generated by EF.
  2. Create a wrapper class that holds a dictionary or implement IDictionary.
  3. Implement the Add function so it also adds the value to the list that EF uses.
  4. The first time a method that operates on the Persons list or dictionary is called make sure they are properly initialized

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
  • if your going to access the list of Persons directly then you need to also modify your partial class so that adding to the list will add to the dictionary (perhaps using a wrapper for the Persons list or a wrapper class all together)
  • there are some improvements to be made if dealing with large data sets or needing optimization, eg not deleting/re-adding all elements when setting new dictionary
  • you might need to implement other events and custom logic depending on your requirements

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM