繁体   English   中英

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

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

让我们假设我们有一个类,如下所示:

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

    // Other code
}

现在,假设我们想要使用 EF Core Code First 来保持它,并且我们使用的是像 SQL Server 这样的 RDMBS。

一种可能的方法显然是创建一个包装器类Wraper来包装字符串:

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

    public string Value { get; set; }
}

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

尽管这可行,但我不太喜欢这种方法,因为出于持久性考虑,我们正在更改一个非常简单的模型。 确实,仅考虑域模型和代码,如果没有持久性, Wraper类在那里毫无意义。

除了创建包装器类之外,还有其他方法可以使用 EF Core Code First 将带有字符串列表的实体持久化到 RDBMS 吗? 当然,最后必须做同样的事情:必须创建另一个表来保存字符串,并且必须建立“一对多”关系。 我只想使用 EF Core 执行此操作,而无需在域模型中对包装器类进行编码。

这可以从 Entity Framework Core 2.1开始以更简单的方式实现。 EF 现在支持值转换来专门解决这样的场景,其中属性需要映射到不同的类型以进行存储。

要保留字符串集合,您可以通过以下方式设置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);
} 

请注意,此解决方案不会将您的业务类与 DB 问题混为一谈。

不用说,这个解决方案必须确保字符串不能包含分隔符。 但是,当然,可以使用任何自定义逻辑来进行转换(例如,从/到 JSON 的转换)。

另一个有趣的事实是,空值不会传递到转换例程中,而是由框架本身处理。 因此无需担心转换例程中的空检查。 但是,如果数据库包含NULL值,则整个属性将变为null

您可以在存储库中使用非常有用的AutoMapper来实现这一点,同时保持整洁。

类似的东西:

我的实体.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; }
}

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

最后,使用 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);
    }
}

你是对的,你不想让你的域模型充满持久性问题。 事实是,如果您对域和持久性使用相同的模型,您将无法避免这个问题。 特别是使用实体框架。

解决方案是,在完全不考虑数据库的情况下构建域模型。 然后构建一个单独的层来负责翻译。 类似于“存储库”模式的东西。

当然,现在你有两倍的工作。 因此,您需要在保持模型清洁和进行额外工作之间找到正确的平衡点。 提示:在更大的应用程序中,额外的工作是值得的。

这可能为时已晚,但您永远无法确定它可能对谁有所帮助。 根据之前的答案查看我的解决方案

首先,您将需要using System.Collections.ObjectModel;此引用using System.Collections.ObjectModel;

然后扩展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);
    }
}

然后创建一个抽象的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);
    }

}

然后像这样使用它

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

扩展已经接受的在ValueConverter中添加ValueConverterOnModelCreating 您可以为所有实体绘制此映射,而不仅仅是显式实体,并且您可以支持存储分隔字符:

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

所以最终结果是数据库中的一个序列化的字符串数组。 这种方法也适用于其他可序列化类型( Dictionary<string, string> 、简单的DTOPOCO对象......

我内心深处有一个纯粹主义者,他对将序列化数据持久化到数据库中很生气,但我已经不时地忽略它。

我通过创建一个新的StringBackedList类来实现一个可能的解决方案,其中实际的列表内容由一个字符串支持。 它通过在修改列表时更新后备字符串来工作,使用Newtonsoft.Json作为序列化程序(因为我已经在我的项目中使用了它,但任何都可以工作)。

您可以像这样使用列表:

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

这是StringBackedList类的实现。 为便于使用,支持属性通过引用传递,使用此解决方案

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

注意事项:仅在修改列表本身时才会更新支持字符串。 通过直接访问(例如通过列表索引器)更新列表元素需要手动调用Store()方法。

我找到了一个技巧,我认为这是解决此类问题的非常有用的解决方法:

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

如何使用:

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

在数据库中

表用户:

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

暂无
暂无

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

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