简体   繁体   English

如何使用 Entity Framework Core 保留字符串列表?

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

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.现在,假设我们想要使用 EF Core Code First 来保持它,并且我们使用的是像 SQL Server 这样的 RDMBS。

One possible approach is obviously to create a wraper class Wraper which wraps the string:一种可能的方法显然是创建一个包装器类Wraper来包装字符串:

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.并重构该类,使其现在依赖于Wraper对象列表。 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.在这种情况下,EF 将为Entity生成一个表,为Wraper生成一个表并建立“一对多”关系:对于每个实体,都有一堆包装器。

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.确实,仅考虑域模型和代码,如果没有持久性, Wraper类在那里毫无意义。

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?除了创建包装器类之外,还有其他方法可以使用 EF Core Code First 将带有字符串列表的实体持久化到 RDBMS 吗? 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.我只想使用 EF Core 执行此操作,而无需在域模型中对包装器类进行编码。

This can be achieved in a much more simple way starting with Entity Framework Core 2.1 .这可以从 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. EF 现在支持值转换来专门解决这样的场景,其中属性需要映射到不同的类型以进行存储。

To persist a collection of strings, you could setup your DbContext in the following way:要保留字符串集合,您可以通过以下方式设置DbContext

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.请注意,此解决方案不会将您的业务类与 DB 问题混为一谈。

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 (eg conversion from/to JSON).但是,当然,可以使用任何自定义逻辑来进行转换(例如,从/到 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.但是,如果数据库包含NULL值,则整个属性将变为null

You could use the ever useful AutoMapper in your repository to achieve this while keeping things neat.您可以在存储库中使用非常有用的AutoMapper来实现这一点,同时保持整洁。

Something like:类似的东西:

MyEntity.cs我的实体.cs

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

MyEntityDto.cs 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:在 Startup.cs 中设置 AutoMapper 映射配置:

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:最后,使用 MyEntityRepository.cs 中的映射,这样您的业务逻辑就不必知道或关心如何处理 List 以实现持久性:

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);
    }
}

You are right, you do not want to litter your domain model with persistence concerns.你是对的,你不想让你的域模型充满持久性问题。 The truth is, if you use your same model for your domain and persistence, you will not be able to avoid the issue.事实是,如果您对域和持久性使用相同的模型,您将无法避免这个问题。 Especially using Entity Framework.特别是使用实体框架。

The solution is, build your domain model without thinking about the database at all.解决方案是,在完全不考虑数据库的情况下构建域模型。 Then build a separate layer which is responsible for the translation.然后构建一个单独的层来负责翻译。 Something along the lines of the 'Repository' pattern.类似于“存储库”模式的东西。

Of course, now you have twice the work.当然,现在你有两倍的工作。 So it is up to you to find the right balance between keeping your model clean and doing the extra work.因此,您需要在保持模型清洁和进行额外工作之间找到正确的平衡点。 Hint: The extra work is worth it in bigger applications.提示:在更大的应用程序中,额外的工作是值得的。

This might be late, but you can never tell who it might help.这可能为时已晚,但您永远无法确定它可能对谁有所帮助。 See my solution based on the previous answer根据之前的答案查看我的解决方案

First, you are going to need this reference using System.Collections.ObjectModel;首先,您将需要using System.Collections.ObjectModel;此引用using System.Collections.ObjectModel;

Then extend the ObservableCollection<T> and add an implicit operator overload for a standard list然后扩展ObservableCollection<T>并为标准列表添加隐式运算符重载

 public class ListObservableCollection<T> : ObservableCollection<T>
{
    public ListObservableCollection() : base()
    {

    }


    public ListObservableCollection(IEnumerable<T> collection) : base(collection)
    {

    }


    public ListObservableCollection(List<T> list) : base(list)
    {

    }
    public static implicit operator ListObservableCollection<T>(List<T> val)
    {
        return new ListObservableCollection<T>(val);
    }
}

Then create an abstract EntityString class (This is where the good stuff happens)然后创建一个抽象的EntityString类(这是好事发生的地方)

public abstract class EntityString
{
    [NotMapped]
    Dictionary<string, ListObservableCollection<string>> loc = new Dictionary<string, ListObservableCollection<string>>();
    protected ListObservableCollection<string> Getter(ref string backingFeild, [CallerMemberName] string propertyName = null)
    {


        var file = backingFeild;
        if ((!loc.ContainsKey(propertyName)) && (!string.IsNullOrEmpty(file)))
        {
            loc[propertyName] = GetValue(file);
            loc[propertyName].CollectionChanged += (a, e) => SetValue(file, loc[propertyName]);
        }
        return loc[propertyName];
    }

    protected void Setter(ref string backingFeild, ref ListObservableCollection<string> value, [CallerMemberName] string propertyName = null)
    {

        var file = backingFeild;
        loc[propertyName] = value;
        SetValue(file, value);
        loc[propertyName].CollectionChanged += (a, e) => SetValue(file, loc[propertyName]);
    }

    private List<string> GetValue(string data)
    {
        if (string.IsNullOrEmpty(data)) return new List<string>();
        return data.Split(';').ToList();
    }

    private string SetValue(string backingStore, ICollection<string> value)
    {

        return string.Join(";", value);
    }

}

Then use it like so然后像这样使用它

public class Categorey : EntityString
{

    public string Id { get; set; }
    public string Name { get; set; }


   private string descriptions = string.Empty;

    public ListObservableCollection<string> AllowedDescriptions
    {
        get
        {
            return Getter(ref descriptions);
        }
        set
        {
            Setter(ref descriptions, ref value);
        }
    }


    public DateTime Date { get; set; }
}

Extending on the already accepted answer of adding a ValueConverter within the OnModelCreating ;扩展已经接受的在ValueConverter中添加ValueConverterOnModelCreating you can have this map out for all entities rather than just explicit ones, and you can support storing delimiting characters:您可以为所有实体绘制此映射,而不仅仅是显式实体,并且您可以支持存储分隔字符:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entity in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var property in entity.ClrType.GetProperties())
        {
            if (property.PropertyType == typeof(List<string>))
            {
                modelBuilder.Entity(entity.Name)
                    .Property(property.Name)
                    .HasConversion(new ValueConverter<List<string>, string>(v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject<List<string>>(v)));
            }
        }
    }
}

So the end result is a serialized array of strings in the database.所以最终结果是数据库中的一个序列化的字符串数组。 This approach can also work on other serializable types as well ( Dictionary<string, string> , simple DTO or POCO objects...这种方法也适用于其他可序列化类型( Dictionary<string, string> 、简单的DTOPOCO对象......

There is a purist deep down somewhere in me that is mad about persisting seralized data into a database, but I have grown to ignore it every once and a while.我内心深处有一个纯粹主义者,他对将序列化数据持久化到数据库中很生气,但我已经不时地忽略它。

I implemented a possible solution by creating a new StringBackedList class, where the actual list content is backed by a string.我通过创建一个新的StringBackedList类来实现一个可能的解决方案,其中实际的列表内容由一个字符串支持。 It works by updating the backing string whenever the list is modified, using Newtonsoft.Json as the serializer (because I already use that in my project, but any would work).它通过在修改列表时更新后备字符串来工作,使用Newtonsoft.Json作为序列化程序(因为我已经在我的项目中使用了它,但任何都可以工作)。

You use the list like this:您可以像这样使用列表:

public class Entity
{
    // that's what stored in the DB, and shouldn't be accessed directly
    public string SomeListOfValuesStr { get; set; }

    [NotMapped]
    public StringBackedList<string> SomeListOfValues 
    {
        get
        {
            // this can't be created in the ctor, because the DB isn't read yet
            if (_someListOfValues == null)
            {
                 // the backing property is passed 'by reference'
                _someListOfValues = new StringBackedList<string>(() => this.SomeListOfValuesStr);
            }
            return _someListOfValues;
        }
    }
    private StringBackedList<string> _someListOfValues;
}

Here's the implementation of the StringBackedList class.这是StringBackedList类的实现。 For ease of use, the backing property is passed by reference, using this solution .为便于使用,支持属性通过引用传递,使用此解决方案

using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Model
{
    public class StringBackedList<T> : IList<T>
    {
        private readonly Accessor<string> _backingStringAccessor;
        private readonly IList<T> _backingList;

        public StringBackedList(Expression<Func<string>> expr)
        {
            _backingStringAccessor = new Accessor<string>(expr);

            var initialValue = _backingStringAccessor.Get();
            if (initialValue == null)
                _backingList = new List<T>();
            else
                _backingList = JsonConvert.DeserializeObject<IList<T>>(initialValue);
        }

        public T this[int index] {
            get => _backingList[index];
            set { _backingList[index] = value; Store(); }
        }

        public int Count => _backingList.Count;

        public bool IsReadOnly => _backingList.IsReadOnly;

        public void Add(T item)
        {
            _backingList.Add(item);
            Store();
        }

        public void Clear()
        {
            _backingList.Clear();
            Store();
        }

        public bool Contains(T item)
        {
            return _backingList.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            _backingList.CopyTo(array, arrayIndex);
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _backingList.GetEnumerator();
        }

        public int IndexOf(T item)
        {
            return _backingList.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            _backingList.Insert(index, item);
            Store();
        }

        public bool Remove(T item)
        {
            var res = _backingList.Remove(item);
            if (res)
                Store();
            return res;
        }

        public void RemoveAt(int index)
        {
            _backingList.RemoveAt(index);
            Store();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _backingList.GetEnumerator();
        }

        public void Store()
        {
            _backingStringAccessor.Set(JsonConvert.SerializeObject(_backingList));
        }
    }

    // this class comes from https://stackoverflow.com/a/43498938/2698119
    public class Accessor<T>
    {
        private Action<T> Setter;
        private Func<T> Getter;

        public Accessor(Expression<Func<T>> expr)
        {
            var memberExpression = (MemberExpression)expr.Body;
            var instanceExpression = memberExpression.Expression;
            var parameter = Expression.Parameter(typeof(T));
            if (memberExpression.Member is PropertyInfo propertyInfo)
            {
                Setter = Expression.Lambda<Action<T>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile();
                Getter = Expression.Lambda<Func<T>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile();
            }
            else if (memberExpression.Member is FieldInfo fieldInfo)
            {
                Setter = Expression.Lambda<Action<T>>(Expression.Assign(memberExpression, parameter), parameter).Compile();
                Getter = Expression.Lambda<Func<T>>(Expression.Field(instanceExpression, fieldInfo)).Compile();
            }

        }

        public void Set(T value) => Setter(value);
        public T Get() => Getter();
    }
}

Caveats : the backing string is only updated when the list itself is modified.注意事项:仅在修改列表本身时才会更新支持字符串。 Updating a list element via direct access (eg via the list indexer) requires a manual call to the Store() method.通过直接访问(例如通过列表索引器)更新列表元素需要手动调用Store()方法。

I have found a trick and I think this is a very usefull workaround to solve this kind of the problem:我找到了一个技巧,我认为这是解决此类问题的非常有用的解决方法:

public class User
{
  public long UserId { get; set; }

  public string Name { get; set; }

  private string _stringArrayCore = string.Empty;

  // Warnning: do not use this in Bussines Model
  public string StringArrayCore
  {
    get
    {
      return _stringArrayCore;
    }

    set
    {
      _stringArrayCore = value;
    }
  }

  [NotMapped]
  public ICollection<string> StringArray
  {
    get
    {
      var splitString = _stringArrayCore.Split(';');
      var stringArray = new Collection<string>();

      foreach (var s in splitString)
      {
        stringArray.Add(s);
      }
      return stringArray;
    }
    set
    {
      _stringArrayCore = string.Join(";", value);
    }
  }
}

How to use:如何使用:

  // Write user
  using (var userDbContext = new UserSystemDbContext())
  {
    var user = new User { Name = "User", StringArray = new Collection<string>() { "Bassam1", "Bassam2" } };
    userDbContext.Users.Add(user);
    userDbContext.SaveChanges();
  }

  // Read User 
  using (var userDbContext = new UserSystemDbContext())
  {
    var user = userDbContext.Users.ToList().Last();

    foreach (var userArray in user.StringArray)
    {
      Console.WriteLine(userArray);
    }
  }

in the database在数据库中

Table Users:表用户:

UserId  | Name | StringArrayCore
1       | User | Bassam1;Bassam2

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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