Supposons que nous ayons une classe qui ressemble à ceci:
public class Entity
{
public IList<string> SomeListOfValues { get; set; }
// Other code
}
Supposons maintenant que nous voulions continuer à utiliser ceci avec EF Core Code First et que nous utilisons un RDMBS comme SQL Server.
Une approche possible consiste évidemment à créer une classe wrapper Wraper
qui enveloppe la chaîne:
public class Wraper
{
public int Id { get; set; }
public string Value { get; set; }
}
Et refactoriser la classe pour qu’elle dépende maintenant d’une liste d’objets Wraper
. Dans ce cas, EF générerait une table pour Entity
, une table pour Wraper
et établirait une relation "un-à-plusieurs": pour chaque entité, il y a un tas de wrappers.
Bien que cela fonctionne, je n'aime pas trop l'approche parce que nous modifions un modèle très simple pour des raisons de persistance. En effet, en pensant uniquement au modèle de domaine et au code, sans la persistance, la classe Wraper
n’a pas de sens.
Existe-t-il un autre moyen de conserver une entité avec une liste de chaînes dans un SGBDR à l'aide de EF Core Code First autre que la création d'une classe d'emballage? Bien sûr, à la fin, la même chose doit être faite: une autre table doit être créée pour contenir les chaînes et une relation "un à plusieurs" doit être en place. Je veux simplement faire cela avec EF Core sans avoir à coder la classe d'emballage dans le modèle de domaine.
Cela peut être réalisé d'une manière beaucoup plus simple à partir d'Entity Framework Core 2.1. EF prend désormais en charge Value Conversions pour traiter spécifiquement des scénarios tels que celui dans lequel une propriété doit être mappée vers un type différent pour le stockage.
Pour conserver une collection de chaînes, vous pouvez configurer votre DbContext
de la manière suivante:
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);
}
Notez que cette solution ne gâche pas votre classe d’affaires avec des préoccupations de base de données.
Inutile de dire que cette solution nécessite de s’assurer que les chaînes ne peuvent pas contenir le délimiteur. Bien entendu, toute logique personnalisée peut être utilisée pour effectuer la conversion (par exemple, conversion de/vers JSON).
Un autre fait intéressant est que les valeurs nulles sont non entrées dans la routine de conversion mais plutôt gérées par le cadre proprement dit. Il n’est donc pas nécessaire de s’inquiéter des vérifications nulles.
Vous pouvez utiliser le très utile AutoMapper de votre référentiel pour y parvenir tout en gardant les choses en ordre.
Quelque chose comme:
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; }
}
Configurez la configuration du mappage AutoMapper dans votre fichier 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))));
Enfin, utilisez le mappage dans MyEntityRepository.cs afin que votre logique d’entreprise n’ait pas à savoir ni à se soucier de la façon dont la liste est gérée pour la persistance:
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);
}
}
Vous avez raison, vous ne voulez pas créer de problèmes de persistance dans votre modèle de domaine. En réalité, si vous utilisez le même modèle pour votre domaine et votre persistance, vous ne pourrez pas éviter le problème. Surtout en utilisant Entity Framework.
La solution est de construire votre modèle de domaine sans penser à la base de données. Ensuite, construisez une couche séparée qui est responsable de la traduction. Quelque chose dans les lignes du motif 'Repository'.
Bien sûr, maintenant vous avez deux fois le travail. Il vous appartient donc de trouver le bon équilibre entre maintenir votre modèle propre et effectuer le travail supplémentaire. Astuce: le travail supplémentaire en vaut la peine dans de plus grandes applications.
C’est peut-être tard, mais vous ne pouvez jamais dire à qui cela pourrait aider… Voir ma solution à partir de la réponse précédente
D'abord, vous allez avoir besoin de cette référence using System.Collections.ObjectModel;
Étendez ensuite le ObservableCollection<T>
et ajoutez une surcharge d'opérateur implicite pour une liste standard
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);
}
}
Créez ensuite une classe abstraite EntityString
(c’est là que les choses intéressantes se passent)
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);
}
}
Alors utilisez-le comme ça
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; }
}
J'ai implémenté une solution possible en créant une nouvelle classe StringBackedList
, dans laquelle le contenu de la liste est sauvegardé par une chaîne. Cela fonctionne en mettant à jour la chaîne de sauvegarde chaque fois que la liste est modifiée, en utilisant Newtonsoft.Json en tant que sérialiseur (car je l'utilise déjà dans mon projet, mais aucun ne fonctionnerait).
Vous utilisez la liste comme ceci:
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;
}
Voici l'implémentation de la classe StringBackedList
. Pour faciliter l'utilisation, la propriété de support est transmise par référence, à l'aide de 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: la chaîne de sauvegarde n'est mise à jour que lorsque la liste elle-même est modifiée. La mise à jour d'un élément de la liste par accès direct (par exemple via l'indexeur de liste) nécessite un appel manuel à la méthode Store()
.