web-dev-qa-db-de.com

Wie kann ich eine Liste von Strings mit Entity Framework Core beibehalten?

Nehmen wir an, wir haben eine Klasse, die wie folgt aussieht:

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

    // Other code
}

Nehmen wir an, wir möchten dies mit EF Core Code First beibehalten und verwenden eine RDMBS wie SQL Server.

Ein möglicher Ansatz besteht offensichtlich darin, eine Wrapper-Klasse Wraper zu erstellen, die die Zeichenfolge umgibt:

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

    public string Value { get; set; }
}

Und die Klasse so umzuwandeln, dass sie jetzt von einer Liste von Wraper-Objekten abhängt. In diesem Fall würde EF eine Tabelle für Entity, eine Tabelle für Wraper generieren und eine "Eins-zu-Viele-Beziehung" herstellen: Für jede Entität gibt es eine Reihe von Wrapern.

Obwohl dies funktioniert, gefällt mir der Ansatz nicht ganz, da wir aus Gründen der Persistenz ein sehr einfaches Modell ändern. Wenn Sie nur über das Domänenmodell und den Code nachdenken, ist die Variable Wraper ohne die Persistenz dort ziemlich bedeutungslos.

Gibt es eine andere Möglichkeit, eine Entität mit einer Liste von Zeichenfolgen in einem RDBMS mit EF Core Code First zu beharren, als eine Wraper-Klasse zu erstellen? Natürlich muss am Ende dasselbe getan werden: Eine andere Tabelle muss erstellt werden, um die Zeichenfolgen zu enthalten, und eine "Eins-zu-Viele-Beziehung" muss vorhanden sein. Ich möchte dies nur mit EF Core tun, ohne die Wraper-Klasse im Domänenmodell codieren zu müssen.

14
user1620696

Dies kann auf sehr viel einfachere Weise erreicht werden, beginnend mit Entity Framework Core 2.1. EF unterstützt jetzt Value Conversions , um speziell auf solche Szenarien einzugehen, bei denen eine Eigenschaft für die Speicherung einem anderen Typ zugeordnet werden muss.

Um eine Sammlung von Zeichenfolgen beizubehalten, können Sie Ihre DbContext folgendermaßen einrichten:

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

Beachten Sie, dass diese Lösung Ihre Geschäftsklasse nicht mit DB-Problemen belastet. 

Es ist unnötig zu erwähnen, dass bei dieser Lösung sichergestellt werden muss, dass die Zeichenfolgen kein Trennzeichen enthalten. Natürlich kann jede benutzerdefinierte Logik verwendet werden, um die Konvertierung durchzuführen (z. B. Konvertierung von/in JSON).

Eine weitere interessante Tatsache ist, dass Nullwerte nicht an die Konvertierungsroutine übergeben werden, sondern eher vom Framework selbst verarbeitet werden. Man muss sich also nicht um Nullprüfungen kümmern.

9
Dejan

Sie können das stets nützliche AutoMapper in Ihrem Repository verwenden, um dies zu erreichen, während Sie die Dinge ordentlich halten.

So etwas wie:

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

Richten Sie die AutoMapper-Mapping-Konfiguration in Ihrer Startup.cs ein:

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

Verwenden Sie schließlich die Zuordnung in MyEntityRepository.cs, damit Ihre Geschäftslogik nicht wissen muss, wie die Liste für die Beständigkeit behandelt wird:

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);
    }
}
4
Steve Land

Sie haben Recht, Sie möchten Ihr Domänenmodell nicht mit Persistenzproblemen belasten. Die Wahrheit ist, wenn Sie dasselbe Modell für Ihre Domäne und Ihre Persistenz verwenden, können Sie das Problem nicht vermeiden. Insbesondere mit dem Entity Framework.

Die Lösung ist, Ihr Domänenmodell zu erstellen, ohne an die Datenbank zu denken. Erstellen Sie dann eine separate Ebene, die für die Übersetzung verantwortlich ist. Etwas entlang des "Repository" -Musters.

Natürlich haben Sie jetzt doppelt so viel Arbeit. Es liegt also an Ihnen, die richtige Balance zwischen dem Reinigen Ihres Modells und der zusätzlichen Arbeit zu finden. Hinweis: In größeren Anwendungen lohnt sich die zusätzliche Arbeit.

3
hatcyl

Dies könnte zu spät sein, aber Sie können nie sagen, wem es helfen könnte. Sehen Sie sich meine Lösung anhand der vorherigen Antwort an

Zuerst benötigen Sie diese Referenz using System.Collections.ObjectModel;

Erweitern Sie dann den ObservableCollection<T> und fügen Sie eine implizite Operatorüberladung für eine Standardliste hinzu

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

Dann erstellen Sie eine abstrakte EntityString-Klasse (hier passiert das Gute)

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

}

Dann benutze es 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; }
}
0
jaytee116

Ich habe eine mögliche Lösung implementiert, indem ich eine neue StringBackedList-Klasse erstellt habe, bei der der tatsächliche Listeninhalt durch eine Zeichenfolge unterlegt ist. Es funktioniert durch Aktualisieren der Hintergrundzeichenfolge, wenn die Liste geändert wird, und zwar mit Newtonsoft.Json als Serialisierer (weil ich das bereits in meinem Projekt verwende, aber es würde funktionieren).

Sie verwenden die Liste wie folgt:

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

Hier ist die Implementierung der Klasse StringBackedList. Zur Vereinfachung der Verwendung wird die Hintergrundeigenschaft als Referenz übergeben, wobei diese Lösung verwendet wird. 

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

Vorbehalte: Die Hintergrundzeichenfolge wird nur aktualisiert, wenn die Liste selbst geändert wird. Das Aktualisieren eines Listenelements über direkten Zugriff (z. B. über den Listenindexierer) erfordert einen manuellen Aufruf der Store()-Methode.

0
Métoule