How to implement code first with existing database - entity-framework

I am using entityframework 5 with code first model and existing database.I am implementing repository pattern.I have a BaseEntity class like below:
public abstract partial class BaseEntity
{
/// <summary>
/// Gets or sets the entity identifier
/// </summary>
public virtual int Id { get; set; }
public override bool Equals(object obj)
{
return Equals(obj as BaseEntity);
}
private static bool IsTransient(BaseEntity obj)
{
return obj != null && Equals(obj.Id, default(int));
}
private Type GetUnproxiedType()
{
return GetType();
}
public virtual bool Equals(BaseEntity other)
{
if (other == null)
return false;
if (ReferenceEquals(this, other))
return true;
if (!IsTransient(this) &&
!IsTransient(other) &&
Equals(Id, other.Id))
{
var otherType = other.GetUnproxiedType();
var thisType = GetUnproxiedType();
return thisType.IsAssignableFrom(otherType) ||
otherType.IsAssignableFrom(thisType);
}
return false;
}
public override int GetHashCode()
{
if (Equals(Id, default(int)))
return base.GetHashCode();
return Id.GetHashCode();
}
public static bool operator ==(BaseEntity x, BaseEntity y)
{
return Equals(x, y);
}
public static bool operator !=(BaseEntity x, BaseEntity y)
{
return !(x == y);
}
protected virtual void SetParent(dynamic child)
{
}
protected virtual void SetParentToNull(dynamic child)
{
}
protected void ChildCollectionSetter<T>(ICollection<T> collection, ICollection<T> newCollection) where T : class
{
if (CommonHelper.OneToManyCollectionWrapperEnabled)
{
collection.Clear();
if (newCollection != null)
newCollection.ToList().ForEach(x => collection.Add(x));
}
else
{
collection = newCollection;
}
}
protected ICollection<T> ChildCollectionGetter<T>(ref ICollection<T> collection, ref ICollection<T> wrappedCollection) where T : class
{
return ChildCollectionGetter(ref collection, ref wrappedCollection, SetParent, SetParentToNull);
}
protected ICollection<T> ChildCollectionGetter<T>(ref ICollection<T> collection, ref ICollection<T> wrappedCollection, Action<dynamic> setParent, Action<dynamic> setParentToNull) where T : class
{
if (CommonHelper.OneToManyCollectionWrapperEnabled)
return wrappedCollection ?? (wrappedCollection = (collection ?? (collection = new List<T>())).SetupBeforeAndAfterActions(setParent, SetParentToNull));
return collection ?? (collection = new List<T>());
}
}
Now I have a table Customer which has CustomerId as primary key .How should I map such like fields with Id of BaseEntity.If I have a composite key how should I map this to the BaseEntity ID.
Please Help me.

Make sure you set the initialize for your DBContext to false:
context.Database.Initialize(false);
Here is a good article on database initializers for code first:
http://www.codeguru.com/csharp/article.php/c19999/Understanding-Database-Initializers-in-Entity-Framework-Code-First.htm

Related

MongoDB class has argument but none are configured

I have a class with some read only properties that I want to store using mongodb.
User.cs
public class User: ValueObject<User>
{
public UserId Id { get; }
public string Firstname { get; }
public string Lastname { get; }
public User(UserId id, string firstname, string lastname)
{
Id = new UserId(id.Id);
Firstname = firstname;
Lastname = lastname;
}
public User(string id, string firstname, string lastname)
{
Id = new UserId(id);
Firstname = firstname;
Lastname = lastname;
}
protected override bool MembersEquals(User other)
{
return Id == other.Id && Firstname == other.Firstname && Lastname == other.Lastname;
}
protected override int MembersHashCode()
{
return Id.GetHashCode() ^ Firstname.GetHashCode() ^ Lastname.GetHashCode();
}
}
Class map registration:
BsonClassMap.RegisterClassMap<User>(cm =>
{
cm.AutoMap();
cm.MapProperty(user => user.Id);
cm.MapProperty(user => user.Firstname);
cm.MapProperty(user => user.Lastname);
cm.MapCreator(user => new User(user.Id, user.Firstname, user.Lastname));
});
this class is stored as a subdocument. when i try to store the whole document mongodb driver tell me that the MongoDB.Bson.BsonSerializationException: Creator map for class Box.Domain.User has 3 arguments, but none are configured.. I didn't really understand what does it mean.
NB: the UserId class has a registred serializer
public class UserIdBsonSerializer : SerializerBase<UserId>
{
public override UserId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var currentBsonType = context.Reader.GetCurrentBsonType();
return currentBsonType switch
{
BsonType.String => new UserId(context.Reader.ReadString()),
_ => throw new NotSupportedException($"Cannot deserialize {currentBsonType} to an UserId")
};
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, UserId value)
{
context.Writer.WriteString(value.Id);
}
}
EDIT
ValueObject.cs
public abstract class ValueObject<T> : IEquatable<T>
where T : ValueObject<T>
{
// Verify the value object members equality.
protected abstract bool MembersEquals(T other);
// Generate a hash code depending on the value object members values.
protected abstract int MembersHashCode();
#region Equality
public bool Equals([AllowNull] T other)
{
if (ReferenceEquals(other, null))
return false;
if (ReferenceEquals(this, other))
return true;
return MembersEquals(other);
}
public override bool Equals(object obj)
{
var other = obj as T;
return Equals(other);
}
public static bool operator ==(ValueObject<T> lhs, ValueObject<T> rhs)
{
if (ReferenceEquals(lhs, null) && ReferenceEquals(rhs, null))
return true;
if (ReferenceEquals(lhs, null))
return false;
return lhs.Equals(rhs);
}
public static bool operator !=(ValueObject<T> lhs, ValueObject<T> rhs) => !(lhs == rhs);
#endregion
public override int GetHashCode()
{
return MembersHashCode();
}
}
UserId.cs
public class UserId: ValueObject<UserId>
{
public string Id { get; }
public UserId(string id)
{
if (id.Length < 5)
throw new ArgumentException($"user id must be 5 characters length. {id}");
// TODO: Add more validation rules for user id
Id = id;
}
protected override bool MembersEquals(UserId other)
{
return Id == other.Id;
}
protected override int MembersHashCode()
{
return Id.GetHashCode();
}
}
Looks like the cm.AutoMap(); in your register class map is creating 2 creators before adding your own.
If you remove that it'll start working.
BsonClassMap.RegisterClassMap<User>(cm =>
{
cm.MapProperty(user => user.Id);
cm.MapProperty(user => user.Firstname);
cm.MapProperty(user => user.Lastname);
cm.MapCreator(user => new User(user.Id, user.Firstname, user.Lastname));
});
An alternative would be to remove the ImmutableTypeClassMapConvention from the default convention pack.
ConventionRegistry.Remove("__defaults__");
var pack = new ConventionPack();
var defaultConventions = DefaultConventionPack.Instance.Conventions;
pack.AddRange(defaultConventions.Except(
defaultConventions.OfType<ImmutableTypeClassMapConvention>()
));
ConventionRegistry.Register(
"__defaults__",
pack,
t => true);
It is a bit late now but still in case anybody would be interested in the root cause of this issue then please see the following expression && GetMemberType(memberInfos[0]) == parameter.ParameterType.
To summarize:
it is required that both -
constructor parameters be matched to
properties using case insensitive name matching and to be of the exact
same type.
For even more details please see CSHARP-3526

How to query the DbSets of all types that implement an interface?

Many of my data models use this interface:
public interface IHasPrimaryImageProperty
{
PrimaryImageDataModel PrimaryImage { get; set; }
int? PrimaryImageId { get; set; }
}
Where PrimaryImageDataModel is:
public class PrimaryImageDataModel
{
public int Id { get; set; }
public string ImageFile { get; set; }
public int TotalItemsUsingImage { get; set; }
}
I want populate PrimaryImageDataModel.TotalItemsUsingImage, by performing counts on all data models that implement IHasPrimaryImageProperty.
So far I have managed to get a list of types that implement the IHasPrimaryImageProperty.
But I haven't been able to get the total for each Type.
Please see the example below for a demonstration of what I would like to acheive.
public static PrimaryImageDataModel GetImageUsageTotals(PrimaryImageDataModel image)
{
var typesUsingImage = GetTypesWithPrimaryImageProperty();
int totalUsingImage = 0;
foreach (Type typeUsingImage in typesUsingImage)
{
// I WOULD LIKE TO DO SOMETHING LIKE THIS
totalForType = db.Set<typeUsingImage>()
.Where(x => x.PrimaryImageId == image.Id)
.Count()
totalUsingImage += totalForType;
}
image.TotalItemsUsingImage = totalUsingImage;
return image;
}
public static IEnumerable<Type> GetTypesWithPrimaryImageProperty()
{
var currentAssembly = Assembly.GetExecutingAssembly();
foreach (Type type in currentAssembly.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IHasPrimaryImageProperty)))
{
yield return type;
}
}
}
The simplest I see (works in both EF6 and EF Core) is to create a generic method and call it via reflection.
For instance:
static int CountUsage<T>(DbContext db, PrimaryImageDataModel image)
where T : class, IHasPrimaryImageProperty
{
return db.Set<T>()
.Where(x => x.PrimaryImageId == image.Id)
.Count();
}
static readonly MethodInfo CountUsageMethodInfo = typeof(YourClass)
.GetMethod("CountUsage", BindingFlags.NonPublic | BindingFlags.Static);
public static PrimaryImageDataModel GetImageUsageTotals(PrimaryImageDataModel image)
{
var args = new object[] { db, image };
image.TotalItemsUsingImage = GetTypesWithPrimaryImageProperty()
.Sum(type => (int)CountUsageMethodInfo.MakeGenericMethod(type).Invoke(null, args));
return image;
}
IQueryable is covariant. See Variance in Generic Interfaces (C#) This allows an IQueryable<SomeEntity> to be cast to IQueryable<InterfaceType>, for interfaces implemented by that Entity type.
So if you put this method on your EF6 DbContext type:
public IQueryable<T> GetQuery<T>(Type EntityType)
{
return (IQueryable<T>)this.Set(EntityType);
}
Or like this for EF Core:
public IQueryable<T> GetQuery<T>(Type EntityType)
{
var pq = from p in this.GetType().GetProperties()
where p.PropertyType.IsGenericType
&& p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)
&& p.PropertyType.GenericTypeArguments[0] == EntityType
select p;
var prop = pq.Single();
return (IQueryable<T>)prop.GetValue(this);
}
Then you can write
foreach (Type typeUsingImage in typesUsingImage)
{
// I WOULD LIKE TO DO SOMETHING LIKE THIS
totalForType = db.GetQuery<IHasPrimaryImageProperty>(typeUsingImage)
.Where(x => x.PrimaryImageId == image.Id)
.Count()
totalUsingImage += totalForType;
}

Entity framework Generic query in Nongeneric Property

In Entity framework I have objectsets like
public partial class Building
{
public int BuildingID { get; set; }
public string BuildingName { get; set; }
}
public partial class Town
{
public int TownID { get; set; }
public string TownName { get; set; }
}
I want to create a generic query like
T.OrderBy(o=>o.Id).Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
T is generic class can be Building or Town but problem is BuildingId and TownId has different name.I don't want to change their name as Id and create interface IIdentity.
Maybe you could try something like this:
var query = (typeof(T) == typeof(Building) ?
context.Buildings.Select(b => new { Id = b.BuildingId, Name = b.BuildingName }) :
context.Towns.Select(t => new { Id = t.TownId, Name = b.TownName }))
.OrderBy(o => o.Id)...
Not tested but that's worth a test...
You can create generic method which find a field decorated with KeyAttribute, and then performs sorting by found key field. I have tested your model, works perfectly. Look at code snippet.
DbContext:
using System.Collections.Generic;
using System.Data.Entity;
namespace ConsoleApplication28.Entities
{
public class AppDbContext : DbContext
{
public AppDbContext()
{
Database.Connection.ConnectionString = #"Data Source=NOTEBOOK-PC;Initial Catalog=StackOverflowTest;Integrated Security=True";
Database.SetInitializer(new AppDbInitializer());
}
public DbSet<Town> Towns { get; set; }
public DbSet<Building> Buildings { get; set; }
}
public class AppDbInitializer : DropCreateDatabaseIfModelChanges<AppDbContext>
{
protected override void Seed(AppDbContext context)
{
context.Buildings.AddRange(new List<Building>
{
new Building {BuildingName = "Building1"},
new Building {BuildingName = "Building2"},
});
context.Towns.AddRange(new List<Town>
{
new Town {TownName = "Town1"},
new Town {TownName = "Town2"},
});
context.SaveChanges();
base.Seed(context);
}
}
}
Building
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28.Entities
{
public class Building
{
[Key]
public int BuildingID { get; set; }
public string BuildingName { get; set; }
}
}
Town
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28.Entities
{
public class Town
{
[Key]
public int TownID { get; set; }
public string TownName { get; set; }
}
}
Program
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ConsoleApplication28.Entities;
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication28
{
class Program
{
static void Main(string[] args)
{
const int maxDispItem = 10;
const int page = 1;
var db = new AppDbContext();
var towns = db.Towns.OrderByKey().Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
var buildings = db.Buildings.OrderByKey().Skip(maxDispItem * (page - 1)).Take(maxDispItem).ToList();
}
}
public static class Extensions
{
/// <summary>
/// Sorts the elements of a sequence in ascending order according to a key specified using KeyAttribute
/// </summary>
public static IOrderedQueryable<T> OrderByKey<T>(this IQueryable<T> source, bool isAsc = true)
{
var type = typeof(T);
var keyProperty = type.GetProperties().Single(x => x.GetCustomAttributes(typeof(KeyAttribute)).Any());
return source.OrderBy(keyProperty.Name, isAsc);
}
#region COPIED FROM THERE http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property, bool isAsc)
{
return isAsc ? source.OrderBy(property) : source.OrderByDescending(property);
}
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderBy");
}
public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { source, lambda });
return (IOrderedQueryable<T>)result;
}
#endregion
}
}

Handling Related Data when using Entity Framework Code First

I have two Classes: LicenseType and EntityType.
[Table("LicenseType")]
public class LicenseType : ComplianceBase, INotifyPropertyChanged
{
private List<Certification> _certifications = new List<Certification>();
private List<EntityType> _entityTypes = new List<EntityType>();
public List<EntityType> EntityTypes
{
get { return _entityTypes; }
set { _entityTypes = value; }
}
public List<Certification> Certifications
{
get { return _certifications; }
set { _certifications = value; }
}
}
and
[Table("EntityType")]
public class EntityType : ComplianceBase, INotifyPropertyChanged
{
private List<LicenseType> _licenseTypes = new List<LicenseType>();
public List<LicenseType> LicenseTypes
{
get { return _licenseTypes; }
set
{
_licenseTypes = value;
// OnPropertyChanged();
}
}
}
The both derive from ComplianceBase,
public class ComplianceBase
{
private int _id;
private string _name;
private string _description;
public string Description
{
get { return _description; }
set
{
if (_description == value) return;
_description = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public int Id
{
get { return _id; }
set
{
if (value == _id) return;
_id = value;
OnPropertyChanged();
}
}
public string Name
{
get { return _name; }
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
What I want is to be able to do is associate an EntityType with one or more LicenseTypes, so for instance, an EntityType "Primary Lender" could be associated with say two LicenseTypes, "Lender License" and "Mortgage License". In this situation, I want one record in the EntityType table, "Primary Lender" and two records in my LicenseType table: "Lender License" and "Mortgage License".
The code for adding related LicenseTypes to my EntityType is done by calling:
_currentEntity.LicenseTypes.Add(licenseType);
and then calling _context.SaveChanges();
There is an additional table, "EntityTypeLicenseTypes" that serves as the lookup table to relate these two tables. There are two records to join the EntityType with the two related LicenseTypes.
And this works. However, my code also adds (it duplicates) the LicenseType record and adds it in the LicenseType table for those records that are being associated.
How can I stop this from happening?
In order to avoid the duplication you must attach the licenseType to the context:
_context.LicenseTypes.Attach(licenseType);
_currentEntity.LicenseTypes.Add(licenseType);
_context.SaveChanges();

Entity Framework options to map list of strings or list of int (List<string>)

I want to store an object that contains a List of primitives using EF.
public class MyObject {
public int Id {get;set;}
public virtual IList<int> Numbers {get;set;}
}
I know that EF cannot store this, but I'd like to know possible solutions to solve this problem.
The 2 Solutions I can think of are:
1.Create a Dummy object that has an Id and the Integervalue, e.g.
public class MyObject {
public int Id {get;set;}
public virtual IList<MyInt> Numbers {get;set;}
}
public class MyInt {
public int Id {get;set;}
public int Number {get;set;}
}
2.Store the list values as a blob, e.g.
public class MyObject {
public int Id {get;set;}
/// use NumbersValue to persist/load the list values
public string NumbersValue {get;set;}
[NotMapped]
public virtual IList<int> Numbers {
get {
return NumbersValue.split(',');
}
set {
NumbersValue = value.ToArray().Join(",");
}
}
}
The Problem with the 2. approach is, that I have to create a Custom IList implementation to keep track if someone modifies the returned collection.
Is there a better solution for this?
Although I do not like to answer my own question, but here is what solved my problem:
After I found this link about Complex Types I tried several implementations, and after some headache I ended up with this.
The List values get stored as a string on the table directly, so it's not required to perform several joins in order to get the list entries. Implementors only have to implement the conversation for each list entry to a persistable string (see the Code example).
Most of the code is handled in the Baseclass (PersistableScalarCollection). You only have to derive from it per datatype (int, string, etc) and implement the method to serialize/deserialize the value.
It's important to note, that you cannot use the the generic baseclass directly (when you remove the abstract). It seems that EF cannot work with that. You also have to make sure to annotate the derived class with the [ComplexType] attribute.
Also note that it seems not to be possible to implement a ComplexType for IList<T> because EF complains about the Indexer (therefore I went on with ICollection).
It's also important to note, that since everything is stored within one column, you cannot search for values in the Collection (at least on the database). In this case you may skip this implementation or denormalize the data for searching.
Example for a Collection of integers:
/// <summary>
/// ALlows persisting of a simple integer collection.
/// </summary>
[ComplexType]
public class PersistableIntCollection : PersistableScalarCollection<int> {
protected override int ConvertSingleValueToRuntime(string rawValue) {
return int.Parse(rawValue);
}
protected override string ConvertSingleValueToPersistable(int value) {
return value.ToString();
}
}
Usage example:
public class MyObject {
public int Id {get;set;}
public virtual PersistableIntCollection Numbers {get;set;}
}
This is the baseclass that handles the persistence aspect by storing the list entries within a string:
/// <summary>
/// Baseclass that allows persisting of scalar values as a collection (which is not supported by EF 4.3)
/// </summary>
/// <typeparam name="T">Type of the single collection entry that should be persisted.</typeparam>
[ComplexType]
public abstract class PersistableScalarCollection<T> : ICollection<T> {
// use a character that will not occur in the collection.
// this can be overriden using the given abstract methods (e.g. for list of strings).
const string DefaultValueSeperator = "|";
readonly string[] DefaultValueSeperators = new string[] { DefaultValueSeperator };
/// <summary>
/// The internal data container for the list data.
/// </summary>
private List<T> Data { get; set; }
public PersistableScalarCollection() {
Data = new List<T>();
}
/// <summary>
/// Implementors have to convert the given value raw value to the correct runtime-type.
/// </summary>
/// <param name="rawValue">the already seperated raw value from the database</param>
/// <returns></returns>
protected abstract T ConvertSingleValueToRuntime(string rawValue);
/// <summary>
/// Implementors should convert the given runtime value to a persistable form.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
protected abstract string ConvertSingleValueToPersistable(T value);
/// <summary>
/// Deriving classes can override the string that is used to seperate single values
/// </summary>
protected virtual string ValueSeperator {
get {
return DefaultValueSeperator;
}
}
/// <summary>
/// Deriving classes can override the string that is used to seperate single values
/// </summary>
protected virtual string[] ValueSeperators {
get {
return DefaultValueSeperators;
}
}
/// <summary>
/// DO NOT Modeify manually! This is only used to store/load the data.
/// </summary>
public string SerializedValue {
get {
var serializedValue = string.Join(ValueSeperator.ToString(),
Data.Select(x => ConvertSingleValueToPersistable(x))
.ToArray());
return serializedValue;
}
set {
Data.Clear();
if (string.IsNullOrEmpty(value)) {
return;
}
Data = new List<T>(value.Split(ValueSeperators, StringSplitOptions.None)
.Select(x => ConvertSingleValueToRuntime(x)));
}
}
#region ICollection<T> Members
public void Add(T item) {
Data.Add(item);
}
public void Clear() {
Data.Clear();
}
public bool Contains(T item) {
return Data.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex) {
Data.CopyTo(array, arrayIndex);
}
public int Count {
get { return Data.Count; }
}
public bool IsReadOnly {
get { return false; }
}
public bool Remove(T item) {
return Data.Remove(item);
}
#endregion
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator() {
return Data.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator() {
return Data.GetEnumerator();
}
#endregion
}
I'm using EF Core and had a similar problem but solved it in a simpler way.
The idea is to store the list of integers as a comma separated string in the database. I do that by specifying a ValueConverter in my entity type builder.
public class MyObjectBuilder : IEntityTypeConfiguration<MyObject>
{
public void Configure(EntityTypeBuilder<MyObject> builder)
{
var intArrayValueConverter = new ValueConverter<int[], string>(
i => string.Join(",", i),
s => string.IsNullOrWhiteSpace(s) ? new int[0] : s.Split(new[] { ',' }).Select(v => int.Parse(v)).ToArray());
builder.Property(x => x.Numbers).HasConversion(intArrayValueConverter);
}
}
More information can be found here: https://entityframeworkcore.com/knowledge-base/37370476/how-to-persist-a-list-of-strings-with-entity-framework-core-
Bernhard's answer is brilliant. I just couldn't help but refine it a little. Here's my two cents:
[ComplexType]
public abstract class EFPrimitiveCollection<T> : IList<T>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual int Id { get; set; }
const string DefaultValueSeperator = "|";
readonly string[] DefaultValueSeperators = new string[] { DefaultValueSeperator };
[NotMapped]
private List<T> _data;
[NotMapped]
private string _value;
[NotMapped]
private bool _loaded;
protected virtual string ValueSeparator => DefaultValueSeperator;
protected virtual string[] ValueSeperators => DefaultValueSeperators;
[ShadowColumn, MaxLength]
protected virtual string Value // Change this to public if you prefer not to use the ShadowColumnAttribute
{
get => _value;
set
{
_data.Clear();
_value = value;
if (string.IsNullOrWhiteSpace(_value))
return;
_data = _value.Split(ValueSeperators, StringSplitOptions.None)
.Select(x => ConvertFromString(x)).ToList();
if (!_loaded) _loaded = true;
}
}
public EFPrimitiveCollection()
{
_data = new List<T>();
}
void UpdateValue()
{
_value = string.Join(ValueSeparator.ToString(),
_data.Select(x => ConvertToString(x))
.ToArray());
}
public abstract T ConvertFromString(string value);
public abstract string ConvertToString(T value);
#region IList Implementation
public int Count
{
get
{
EnsureData();
return _data.Count;
}
}
public T this[int index]
{
get
{
EnsureData();
return _data[index];
}
set
{
EnsureData();
_data[index] = value;
}
}
public bool IsReadOnly => false;
void EnsureData()
{
if (_loaded)
return;
if (string.IsNullOrWhiteSpace(_value))
return;
if (_data.Count > 0) return;
if (!_loaded) _loaded = true;
_data = _value.Split(ValueSeperators, StringSplitOptions.None)
.Select(x => ConvertFromString(x)).ToList();
}
public void Add(T item)
{
EnsureData();
_data.Add(item);
UpdateValue();
}
public bool Remove(T item)
{
EnsureData();
bool res = _data.Remove(item);
UpdateValue();
return res;
}
public void Clear()
{
_data.Clear();
UpdateValue();
}
public bool Contains(T item)
{
EnsureData();
return _data.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
EnsureData();
_data.CopyTo(array, arrayIndex);
}
public int IndexOf(T item)
{
EnsureData();
return _data.IndexOf(item);
}
public void Insert(int index, T item)
{
EnsureData();
_data.Insert(index, item);
UpdateValue();
}
public void RemoveAt(int index)
{
EnsureData();
_data.RemoveAt(index);
UpdateValue();
}
public void AddRange(IEnumerable<T> collection)
{
EnsureData();
_data.AddRange(collection);
UpdateValue();
}
public IEnumerator<T> GetEnumerator()
{
EnsureData();
return _data.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
EnsureData();
return _data.GetEnumerator();
}
#endregion
}
With that base class you can have as many derivations as you like:
[ComplexType]
public class EFIntCollection : EFPrimitiveCollection<int>
{
public override int ConvertFromString(string value) => int.Parse(value);
public override string ConvertToString(int value) => value.ToString();
}
[ComplexType]
public class EFInt64Collection : EFPrimitiveCollection<long>
{
public override long ConvertFromString(string value) => long.Parse(value);
public override string ConvertToString(long value) => value.ToString();
}
[ComplexType]
public class EFStringCollection : EFPrimitiveCollection<string>
{
string _separator;
protected override string ValueSeparator => _separator ?? base.ValueSeparator;
public override string ConvertFromString(string value) => value;
public override string ConvertToString(string value) => value;
public EFStringCollection()
{
}
public EFStringCollection(string separator)
{
_separator = separator;
}
}
EFPrimitiveCollection works just like a list, so you shouldn't have any issues using it like a normal List. Also the data is loaded on demand.
Here's an example:
if (store.AcceptedZipCodes == null)
store.AcceptedZipCodes = new EFStringCollection();
store.AcceptedZipCodes.Clear();
store.AcceptedZipCodes.AddRange(codes.Select(x => x.Code));
Shadow Column
This attribute is being used to abstract away the Value property. If you do not see the need to do this, simply remove it and make the Value property public.
More information can be found on the ShadowColumnAttribute in my answer here
The simple solution would be in your DataContext (a class that implements DbContext) add this override:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MyObject>()
.Property(p => p.Numbers)
.HasConversion(
toDb => string.Join(",", toDb),
fromDb => fromDb.Split(',').Select(Int32.Parse).ToList() ?? new List<int>());
}