Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to persist a list of strings with Entity Framework Core?

Tags:

Let us suppose that we have one class which looks like the following:

public class Entity {     public IList<string> SomeListOfValues { get; set; }      // Other code } 

Now, suppose we want to persist this using EF Core Code First and that we are using a RDMBS like SQL Server.

One possible approach is obviously to create a wraper class Wraper which wraps the string:

public class Wraper {     public int Id { get; set; }      public string Value { get; set; } } 

And to refactor the class so that it now depends on a list of Wraper objects. In that case EF would generate a table for Entity, a table for Wraper and stablish a "one-to-many" relation: for each entity there is a bunch of wrapers.

Although this works, I don't quite like the approach because we are changing a very simple model because of persistence concerns. Indeed, thinking just about the domain model, and the code, without the persistence, the Wraper class is quite meaningless there.

Is there any other way persist one entity with a list of strings to a RDBMS using EF Core Code First other than creating a wraper class? Of course, in the end the same thing must be done: another table must be created to hold the strings and a "one-to-many" relationship must be in place. I just want to do this with EF Core without needing to code the wraper class in the domain model.

like image 206
user1620696 Avatar asked May 22 '16 04:05

user1620696


People also ask

How does Entity Framework handle lists?

If you have a list, it has to point to some entity. For EF to store the list, it needs a second table. In the second table it will put everything from your list, and use a foreign key to point back to your Test entity. So make a new entity with Id property and MyString property, then make a list of that.

How do I return a list in Entity Framework?

The alternative works because the return type is the same as the entity. The first option can be used if your return type is different from your entity and you'll need a mapping. You can select new Foo() { a = c. PackCostPrice } or whatever you like.

What is OnModelCreating?

The DbContext class has a method called OnModelCreating that takes an instance of ModelBuilder as a parameter. This method is called by the framework when your context is first created to build the model and its mappings in memory.


2 Answers

This can be achieved in a much more simple way starting with Entity Framework Core 2.1. EF now supports Value Conversions to specifically address scenarios like this where a property needs to be mapped to a different type for storage.

To persist a collection of strings, you could setup your DbContext in the following way:

protected override void OnModelCreating(ModelBuilder builder) {     var splitStringConverter = new ValueConverter<IEnumerable<string>, string>(v => string.Join(";", v), v => v.Split(new[] { ';' }));     builder.Entity<Entity>()            .Property(nameof(Entity.SomeListOfValues))            .HasConversion(splitStringConverter); }  

Note that this solution does not litter your business class with DB concerns.

Needless to say that this solution, one would have to make sure that the strings cannot contains the delimiter. But of course, any custom logic could be used to make the conversion (e.g. conversion from/to JSON).

Another interesting fact is that null values are not passed into the conversion routine but rather handled by the framework itself. So one does not need to worry about null checks inside the conversion routine. However, the whole property becomes null if the database contains a NULL value.

What about Value Comparers?

Creating a migration using this converter leads to the following warning:

The property 'Entity.SomeListOfValues' is a collection or enumeration type with a value converter but with no value comparer. Set a value comparer to ensure the collection/enumeration elements are compared correctly.

Setting the correct comparer for the suggested converter depends on the semantics of your list. For example, if you do not care about the order of its elements, you can use the following comparer:

new ValueComparer<IEnumerable<string>>(     (c1, c2) => new HashSet<string>(c1!).SetEquals(new HashSet<string>(c2!)),     c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),     c => c.ToList() ); 

Using this comparer, a reordered list with the same elements would not be detected as changed an thus a roundtrip to the database can be avoided. For more information on the topic of Value Comparers, consider the docs.

like image 91
Dejan Avatar answered Sep 28 '22 13:09

Dejan


You could use the ever useful AutoMapper in your repository to achieve this while keeping things neat.

Something like:

MyEntity.cs

public class MyEntity {     public int Id { get; set; }     public string SerializedListOfStrings { get; set; } } 

MyEntityDto.cs

public class MyEntityDto {     public int Id { get; set; }     public IList<string> ListOfStrings { get; set; } } 

Set up the AutoMapper mapping configuration in your Startup.cs:

Mapper.Initialize(cfg => cfg.CreateMap<MyEntity, MyEntityDto>()   .ForMember(x => x.ListOfStrings, opt => opt.MapFrom(src => src.SerializedListOfStrings.Split(';')))); Mapper.Initialize(cfg => cfg.CreateMap<MyEntityDto, MyEntity>()   .ForMember(x => x.SerializedListOfStrings, opt => opt.MapFrom(src => string.Join(";", src.ListOfStrings)))); 

Finally, use the mapping in MyEntityRepository.cs so that your business logic doesnt have to know or care about how the List is handled for persistence:

public class MyEntityRepository {     private readonly AppDbContext dbContext;     public MyEntityRepository(AppDbContext context)     {         dbContext = context;     }      public MyEntityDto Create()     {         var newEntity = new MyEntity();         dbContext.MyEntities.Add(newEntity);          var newEntityDto = Mapper.Map<MyEntityDto>(newEntity);          return newEntityDto;     }      public MyEntityDto Find(int id)     {         var myEntity = dbContext.MyEntities.Find(id);          if (myEntity == null)             return null;          var myEntityDto = Mapper.Map<MyEntityDto>(myEntity);          return myEntityDto;     }      public MyEntityDto Save(MyEntityDto myEntityDto)     {         var myEntity = Mapper.Map<MyEntity>(myEntityDto);          dbContext.MyEntities.Save(myEntity);          return Mapper.Map<MyEntityDto>(myEntity);     } } 
like image 26
Steve Land Avatar answered Sep 28 '22 13:09

Steve Land