AutoMapper using EF Core persists and inheritance - c#

I am using
.NET Core v3.1
EF Core v3
AutoMapper v9
AutoMapper.Collection.EntityFrameworkCore v1.0.1
and I am trying to add polymorph entities.
I have the following entities:
public abstract class Base
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Child1 : Base
{
public string Name2 { get; set; }
}
And corresponding dtos
public abstract class BaseDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Child1Dto : BaseDto
{
public string Name2 { get; set; }
}
I am registering them like that:
CreateMap<Base, BaseDto>()
.IncludeAllDerived()
.ReverseMap()
.IncludeAllDerived();
CreateMap<Child1, Child1Dto>()
.ReverseMap();
And also add do db context like that with discriminator:
modelBuilder.Entity<Base>()
.HasDiscriminator<string>("type")
.HasValue<Child1>("child1");
After that I try to add a new entry to the database like that:
var dto = new Child1Dto()
{
Name = "Name",
Name2 = "Name2",
};
var addedOrInserted = db.Set<Base>().Persist(_mapper).InsertOrUpdate(dto);
But for that case, I get the following exception:
Expression of type 'AutoMapper.EquivalencyExpression.EquivalentExpression2[Child1]' cannot be used for parameter of type 'AutoMapper.EquivalencyExpression.IEquivalentComparer2[Base]'
I think that this happens because the InsertOrUpdate is checking if the entity exists to check if an insert or an update should be performed.
Do you have any solution for that?

It seems to be because you used CreateMap<Child1,Child1Dto>().ReverseMap(); when registering.
Even if they are inheritance relationship, it is not possible to directly add Child1Dto to Base, I think it may be necessary to use db.Set<Child1>.
Try to add public DbSet<Child1> Child1s { get; set; }in your context.
Then in your controller:
Child1Dto basedto = new Child1Dto()
{
Id = new Guid(),
Name = "Name",
Name2 = "Name2"
};
context.Child1s.Persist(mapper).InsertOrUpdate(basedto);
context.SaveChanges();
Result:

Related

Is is good practice to have same type of list in a class?

I have created a class and I'm putting a list of same type as a property of that class.
Is it good or bad practice?
I am putting the same type of list because of I want to manage everything by only one object.
I don't want to create a single object and a list of object of the same type.
Any help is highly appreciated!
class AssetSection
{
public string Code { get; set; }
public string Description { get; set; }
public string SITEID { get; set; }
public string PlantID { get; set; }
public string User { get; set; }
public string UpDateTime { get; set; }
public List<AssetSection> AssetSections { get; set; }
public AssetSection(string des, string code)
{
Description = des;
Code = code;
}
}
That's ok. If you can imagine, you can design and use it.
Let's talk about entity framework. We create 2 entities like this:
public class User : IdentityUser
{
[Key]
public string Id { get; set; }
public UserProfile Profile { get; set; }
}
public class UserProfile
{
[Key]
public string UserId { get; set; }
public User User { get; set; }
}
Now, when we try to get current user:
User user = await _userManager.GetUserAsync(User);
user becomes an instance of User class now. This instance has a property name Profile, and this property has another property name User which has a type User.
It's called mapping. So, to answer your question: You can use it. But I'm not saying it's good or not based on the way to design the model.
As a general observation, such a structure is known as a rose tree, or just a tree. It enables you to write code like this:
var t = new AssetSection("foo", "bar")
{
AssetSections = new List<AssetSection>
{
new AssetSection("baz", "qux")
{
new AssetSection("corge", "garply"),
new AssetSection("fred", "plugh")
{
AssetSections = new List<AssetSection>
{
new AssetSection("xyzzy", "thud")
}
}
},
new AssetSection("quux", "quuz")
{
new AssetSection("grault", "waldo")
}
}
};
If what you want to model is a tree-like structure like that, then it's fine. On the other hand, if such a hierarchy is not what you're trying to model, then it's likely to be confusing.
By the way, the code as proposed violates the .NET framework design guidelines:
DO NOT provide settable collection properties.
DO NOT use ArrayList or List<T> in public APIs

Map JSON column from MySql Database to C# Class from Web Api

I have a MySql database with columns Id int and Name:json
Places Table Sample
Id Name
1 {"en":"Sphinx","ar":"أبو الهول","fr":"Le sphinx"}
C# Place class
public class Place
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
}
I'm connecting with EntityFramework 6 and connection success and retrieve data like this
{Id = 1, Name = "{\"en\":\"Sphinx\", \"ar\":\"أبو الهول\", \"fr\":\"Le sphinx\"}" }
What I want how to Map Name to new Object not JSON string
something like this
Place class
public class Place
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public Localized<string> Name { get; set; }
}
Localized class
public class Localized<T>
{
public T en { get; set; } // english localization
public T ar { get; set; } // arabic localization
public T fr { get; set; } // french localization
}
when I do this Name property come with NULL value
Code in Repository
using (var context = new PlacesEntityModel())
{
return context.Places.Take(5).ToList();
}
I don't want to use AutoMapper,
I want something in EntityFramework to select only one language in Database Level without fetching all other data and then map it
how to fix this?
You can try extension method to map from your entity type.
public class Place
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
}
public class PlaceDTO
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public Localized<string> Name { get; set; }
}
public class Localized<T>
{
public T en { get; set; } // english localization
public T ar { get; set; } // arabic localization
public T fr { get; set; } // french localization
}
Extenstion Method ToDto
public static class Extensions
{
public static PlaceDTO ToDto(this Place place)
{
if (place != null)
{
return new PlaceDTO
{
Id = place.Id,
Name = JsonConvert.DeserializeObject<Localized<string>>(place.Name)
};
}
return null;
}
}
Usage
var place = new Place() { Id = 1, Name = "{\"en\":\"Sphinx\", \"ar\":\"أبو الهول\", \"fr\":\"Le sphinx\"}" };
var placeDTO = place.ToDto();
Console.WriteLine($"{placeDTO.Id}-{placeDTO.Name.ar}-{placeDTO.Name.en}-{placeDTO.Name.fr}");
First of all, by using a class with a property per language, you restrict yourself. You'd always have to add new properties if you add new languages, which would of course be feasible, but unnecessary complicated. Furthermore you'd usually have the language as a string-ish object (or be able to convert), hence this would lead to code like this
Localized<string> name = ...;
switch(language)
{
case "en":
return name.en;
case "ar":
return name.ar;
case "fr":
return name.fr;
default:
throw new LocalizationException();
}
which is error-prone and overly complicated. For your problem, I think I'd opt to use some kind of dictionary
IDictionary<string, string> names = ...;
if(names.ContainsKey(language))
{
return names[language];
}
else
{
throw new LocalizationException();
}
which is easily extensible by just adding more translations to the dictionary.
To convert your JSON string to an IDcitionary<string, string>, you could use the following code
localizedNames = JObject.Parse(Name)
.Children()
.OfType<JProperty>()
.ToDictionary(property => property.Name,
property => property.Value.ToString());
From within your class this would effectively be
public class Place
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
public Dictionary<string, string> LocalizedNames
{
get
{
return JObject.Parse(Name)
.Children()
.OfType<JProperty>()
.ToDictionary(property => property.Name,
property => property.Value.ToString());
}
}
}
The localized values can be accessed like
var localizedPlaceName = place.LocalizedNames[language];
Please note: Depending on your needs and use cases, you should consider the following issues:
Caching
In my snippet, the JSON string is parsed every time the localized names are accessed. Depending on how often you access it, this might be detrimental to performance, which could be mitigated by caching the result (don't forget to delete the cache when Name is set).
Separation of concerns
The class as is is supposed to be a pure model class. You might want to introduce domain classes that encapsulate the presented logic, rather than adding the logic to the model class. Having a factory that creates readily localized objects based on the localizable object and the language could be an option, too.
Error handling
In my code there is no error handling. Depending on the reliability of input you should consider additional error handling.
devart.com/dotconnect/mysql/docs/EF-JSON-Support.html
Like what #Nkosi said
In that case then, take a look at this article devart.com/dotconnect/mysql/docs/EF-JSON-Support.html
It probably can given that the library was able to build that feature in. You would need to figure out what they they did (reverse engineer)
I usually just use JSON.Net, I notice that another answer referenced JObject, but without going into whether your data-model is the right model, I generally find that you can do:
var MyObjectInstance = JObject.Parse(myJsonString).ToObject<MyObjectType>();
I notice that you have ComponentModel attributes on your class. I don't know off hand how many of these JSon.Net supports, and you'd have to research that. It definitely supports some attributes from XML serialization, and also has some of it's own.
Note that you can also convert a JSOn array into a list:
var MyObjectList = JArray.Parse(myJsonString).ToObject<IEnumerable<MyObjectType>();
I want something in EntityFramework to select only one language in
Database Level without fetching all other data and then map it
if you want it to be from database level, you can always create a view and then include this view in your project.
Example :
CREATE VIEW `PlacesLocalized` AS
SELECT
Id
, TRIM(REPLACE(name->'$.en', '"','')) AS en
, TRIM(REPLACE(name->'$.ar', '"','')) AS ar
, TRIM(REPLACE(name->'$.fr', '"','')) AS fr
FROM
places
This would create a model class Like :
public class PlacesLocalized
{
public int Id { get; set; }
public string en {get; set;}
public string ar {get; set;}
public string fr {get; set;}
}
Then, you can do :
var places = context.PlacesLocalized.Where(x=> x.en == "Sphinx");
But if you don't have enough permissions to do this in the database level, then you would need to specify the query in your EF. There is no easy way to change the execution logic of Entity Framework just for specific classes. That's why Entity Framework included SqlQuery method, which would give more flexibility to have custom queries when needed (like yours).
So, if you need to specify the localization from Entity Framework, then you would do a repository class to specify all custom queries you need including creating any DTO needed.
The basic way would be something like this :
public enum Localized
{
English,
Arabic,
French
}
public class PlaceRepo : IDisposable
{
private readonly PlacesEntityModel _context = new PlacesEntityModel();
public List<Place> GetPlacesLocalized(Localized localized = Localized.English)
{
string local = localized == Localized.Arabic ? "$.ar"
: localized == Localized.French ? "$.fr"
: "$.en";
return _context.Places.SqlQuery("SELECT Id, name-> #p0 as Name FROM places", new[] { local })
.Select(x=> new Place { Id = x.Id, Name = x.Name.Replace("\"", string.Empty).Trim() })
.ToList();
}
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
_disposed = true;
}
}
~PlaceRepo()
{
Dispose(false);
}
}
now, you can do this :
using(var repo = new PlaceRepo())
{
var places = repo.GetPlacesLocalized(Localized.Arabic);
}
public class Place
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
public static explicit operator Place(PlaceDTO dto)
{
return new Place()
{
Id = dto.Id,
Name = dto.Name
};
}
}
public class PlaceDTO
{
[Key, Column("id")]
public int Id { get; set; }
[Column("name")]
public Localized<string> Name { get; set; }
public static explicit operator PlaceDTO(Place pls)
{
return new PlaceDTO()
{
Id = pls.Id,
Name = pls.Name
};
}
}
var placeDTO = (placeDto)place;
we can achieve this using explicit operator without using auto mapper

Use Dapper.Contrib with inheritance

When trying to use Contrib's CRUD methods in an object where the properties are in an inherited object I get an
Entity must have at least one [Key] or [ExplicitKey] property
error. Here is a simplified version of my objects:
public class BaseObject
{
public string Delete()
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["con"].ConnectionString))
{
db.Delete(this);
}
}
}
and this
public class Product: BaseObject
{
[Key]
public int id { get; set; }
public string title { get; set; }
public string name { get; set; }
}
I get the error when I execute:
Product product = new Product() {id = 1};
product.Delete();
If I Remove the inheritance and move the Delete() method into the Product object it works flawlessly.
Any ideas?
Your BaseObject is not linked to any table so calling Delete() on it could not be understood by Dapper.
I think that in your case, I would simply use an extension method:
public static class BaseObjectExtensions
{
public static string Delete<T>(this T theObject) where T : BaseObject
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["con"].ConnectionString))
{
db.Delete(theObject);
}
}
}

How to use Moq with Entity Framework Include()

I am trying write unit test for my DAL layer.
The complication is that the DAL Layer has a query which uses Include().
I don't know how to mock the Include() method.
Models
public class Apps
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DataMember]
public int ID { get; set; }
[DataMember]
[Required(ErrorMessage = "App name required.")]
public string Name { get; set; }
public virtual ICollection<AppDataPermission> AppDataPermissions { get; set; }
}
public class AppDataPermission{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DataMember]
public int ID { get; set; }
public DataPermissions DataPermission { get; set; }
public virtual Apps App { get; set; }
}
public enum DataPermissions
{
Admin = 1,
Support = 2
}
DAL
public List<Apps> GetApps()
{
var apps = dbContext.Apps
.Include(x => x.AppDataPermission)
.ToList();
return apps;
}
I tried following [https://msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx][1]
But I get following error
System.ArgumentNullException occurred
HResult=0x80004003
Message=Value cannot be null.
Parameter name: source
Source=EntityFramework
StackTrace:
at System.Data.Entity.Utilities.Check.NotNull[T](T value, String parameterName)
at System.Data.Entity.QueryableExtensions.Include[T,TProperty](IQueryable`1 source, Expression`1 path)
The problem with mocking the database access code is that the you are mocking the part that is the most complicated (linq-to-sql, navigation properties).
I recommend, generally speaking, not to expose DAL objects.
Anyway, you haven't shown your mocking code, but I'm assuming that you are mocking public List<Apps> GetApps().
Here is one method to mock it:
var mockRepo = new Mock<IMyAmazingRepository>(MockBehavior.Strict);
var myMockedApps = new List<App> () {
new Apps { ID = 1, Name ="One", new List<AppDataPermission> { (...) },
new Apps { ID = 2, Name ="Two", new List<AppDataPermission> { (...) }
};
mockRepo.Setup(m => m.GetApps(_loggedInUserId)).Returns(myMockedApps);

How to store JSON in an entity field with EF Core?

I am creating a reusable library using .NET Core (targeting .NETStandard 1.4) and I am using Entity Framework Core (and new to both). I have an entity class that looks like:
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
and I have a DbContext class that defines the DbSet:
public DbSet<Campaign> Campaigns { get; set; }
(I am also using the Repository pattern with DI, but I don't think that is relevant.)
My unit tests give me this error:
System.InvalidOperationException: Unable to determine the relationship
represented by navigation property 'JToken.Parent' of type
'JContainer'. Either manually configure the relationship, or ignore
this property from the model..
Is there a way to indicate that this is not a relationship but should be stored as a big string?
Going to answer this one differently.
Ideally the domain model should have no idea how data is stored. Adding backing fields and extra [NotMapped] properties is actually coupling your domain model to your infrastructure.
Remember - your domain is king, and not the database. The database is just being used to store parts of your domain.
Instead you can use EF Core's HasConversion() method on the EntityTypeBuilder object to convert between your type and JSON.
Given these 2 domain models:
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
public IList<Address> Addresses { get; set; }
}
public class Address
{
public string Type { get; set; }
public string Company { get; set; }
public string Number { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
I have only added attributes that the domain is interested in - and not details that the DB would be interested in; I.E there is no [Key].
My DbContext has the following IEntityTypeConfiguration for the Person:
public class PersonsConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
// This Converter will perform the conversion to and from Json to the desired type
builder.Property(e => e.Addresses).HasConversion(
v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
}
}
With this method you can completely decouple your domain from your infrastructure. No need for all the backing field & extra properties.
The key to making the the Change Tracker function correctly is to implement a ValueComparer as well as a ValueConverter. Below is an extension to implement such:
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<T>(v) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonConvert.SerializeObject(l) == JsonConvert.SerializeObject(r),
v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(),
v => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(v))
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}
Example of how this works.
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public string Type { get; set; }
public string Company { get; set; }
public string Number { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
public class PersonsConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
// This Converter will perform the conversion to and from Json to the desired type
builder.Property(e => e.Addresses).HasJsonConversion<IList<Address>>();
}
}
This will make the ChangeTracker function correctly.
#Michael's answer got me on track but I implemented it a little differently. I ended up storing the value as a string in a private property and using it as a "Backing Field". The ExtendedData property then converted JObject to a string on set and vice versa on get:
public class Campaign
{
// https://learn.microsoft.com/en-us/ef/core/modeling/backing-field
private string _extendedData;
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[NotMapped]
public JObject ExtendedData
{
get
{
return JsonConvert.DeserializeObject<JObject>(string.IsNullOrEmpty(_extendedData) ? "{}" : _extendedData);
}
set
{
_extendedData = value.ToString();
}
}
}
To set _extendedData as a backing field, I added this to my context:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Campaign>()
.Property<string>("ExtendedDataStr")
.HasField("_extendedData");
}
Update: Darren's answer to use EF Core Value Conversions (new to EF Core 2.1 - which didn't exist at the time of this answer) seems to be the best way to go at this point.
For those using EF 2.1 there is a nice little NuGet package EfCoreJsonValueConverter that makes it pretty simple.
using Innofactor.EfCoreJsonValueConverter;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
public class CampaignConfiguration : IEntityTypeConfiguration<Campaign>
{
public void Configure(EntityTypeBuilder<Campaign> builder)
{
builder
.Property(application => application.ExtendedData)
.HasJsonValueConversion();
}
}
I have made a solution based on Robert Raboud's contribution. The change made by me is that my implementation uses a HasJsonConversion method that depends on the System.Text.Json package rather than Newtonsofts library:
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true
};
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<T>(v, options) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonSerializer.Serialize(l, options) == JsonSerializer.Serialize(r, options),
v => v == null ? 0 : JsonSerializer.Serialize(v, options).GetHashCode(),
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, options), options)
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("LONGTEXT");
return propertyBuilder;
}
Note also that this implementation expects for the column to be LONGTEXT since I am using a MySQL setup.
Here's something I used
Model
public class FacilityModel
{
public string Name { get; set; }
public JObject Values { get; set; }
}
Entity
[Table("facility", Schema = "public")]
public class Facility
{
public string Name { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
}
Mapping
this.CreateMap<Facility, FacilityModel>().ReverseMap();
DBContext
base.OnModelCreating(builder);
builder.Entity<Facility>()
.Property(b => b.Values)
.HasColumnType("jsonb")
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
Could you try something like this?
[NotMapped]
private JObject extraData;
[NotMapped]
public JObject ExtraData
{
get { return extraData; }
set { extraData = value; }
}
[Column("ExtraData")]
public string ExtraDataStr
{
get
{
return this.extraData.ToString();
}
set
{
this.extraData = JsonConvert.DeserializeObject<JObject>(value);
}
}
here is the migration output:
ExtraData = table.Column<string>(nullable: true),
For developers, who work with EF Core 3.1 and meet such error ("The entity type 'XXX' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.") the solution is:
Move .HasConversion() method with it's lambda from:
OrderConfiguration : IEntityTypeConfiguration<T> to
OnModelCreating(ModelBuilder modelBuilder) in your DataContext.
// DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var entityTypes = modelBuilder.Model.GetEntityTypes();
foreach (var entityType in entityTypes)
{
foreach (var property in entityType.ClrType.GetProperties().Where(x => x != null && x.GetCustomAttribute<HasJsonConversionAttribute>() != null))
{
modelBuilder.Entity(entityType.ClrType)
.Property(property.PropertyType, property.Name)
.HasJsonConversion();
}
}
base.OnModelCreating(modelBuilder);
}
Create an attribute to handle the properties of the entities.
public class HasJsonConversionAttribute : System.Attribute
{
}
Create extention class to find Josn properties
public static class ValueConversionExtensions
{
public static PropertyBuilder HasJsonConversion(this PropertyBuilder propertyBuilder)
{
ParameterExpression parameter1 = Expression.Parameter(propertyBuilder.Metadata.ClrType, "v");
MethodInfo methodInfo1 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("SerializeObject", types: new Type[] { typeof(object) });
MethodCallExpression expression1 = Expression.Call(methodInfo1 ?? throw new Exception("Method not found"), parameter1);
ParameterExpression parameter2 = Expression.Parameter(typeof(string), "v");
MethodInfo methodInfo2 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("DeserializeObject", 1, BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, CallingConventions.Any, types: new Type[] { typeof(string) }, null)?.MakeGenericMethod(propertyBuilder.Metadata.ClrType) ?? throw new Exception("Method not found");
MethodCallExpression expression2 = Expression.Call(methodInfo2, parameter2);
var converter = Activator.CreateInstance(typeof(ValueConverter<,>).MakeGenericType(typeof(List<AttributeValue>), typeof(string)), new object[]
{
Expression.Lambda( expression1,parameter1),
Expression.Lambda( expression2,parameter2),
(ConverterMappingHints) null
});
propertyBuilder.HasConversion(converter as ValueConverter);
return propertyBuilder;
}
}
Entity example
public class Attribute
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
[HasJsonConversion]
public List<AttributeValue> Values { get; set; }
}
public class AttributeValue
{
public string Value { get; set; }
public IList<AttributeValueTranslation> Translations { get; set; }
}
public class AttributeValueTranslation
{
public string Translation { get; set; }
public string CultureName { get; set; }
}
Download Source
For those who are working on entity framework core 5.0 and above. below can work if you are getting error like below
The entity type '{EntityName}' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. The error is asking to define a primary key on the model
Try this
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Person>(
eb =>
{
eb.Property(p => p.Addresses).HasConversion(
v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
);
});
}
----------------- 2022 Update -----------------
Hey there,
Just sharing an update from Dec/2022.
Recently, EF Core 7.0 was launched, containing one of the most awaited features called JSON Columns.
This new feature allows us for mapping aggregates (written from .NET types) into JSON documents.
Just remember that, In EF Core, aggregate types are defined using Owned Entity Types.
https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities
Let's consider this scenario: A object called "LogDetail", that needs to be stored into a single column as a JSON, inside another object (or table) called "Log".
public class Log : BaseEntity
{
public string TraceID { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public LogDetail LogDetail { get; set; } = null!;
public string IpAddress { get; set; } = string.Empty;
}
public class LogDetail
{
public string InnerException { get; set; } = null!;
public string MemberMap { get; set; } = null!;
public string Message { get; set; } = null!;
public string Source { get; set; } = null!;
public string StackTrace { get; set; } = null!;
public string TypeMap { get; set; } = null!;
public string Path { get; set; } = null!;
}
By the end, all that you need is to configure the behavior on your configuration map class:
public sealed class LogMap : IEntityTypeConfiguration<Log>
{
public override void Configure(EntityTypeBuilder<Log> builder)
{
/* Owned Type Configurations */
builder.OwnsOne(e => e.LogDetail, options =>
{
options.ToJson("LOG_DETAIL");
});
}
}
The comment by #Métoule:
Be careful with this approach: EF Core marks an entity as modified only if the field is assigned to. So if you use person.Addresses.Add, the entity won't be flagged as updated; you'll need to call the property setter person.Addresses = updatedAddresses.
made me take a different approach so that this fact is obvious: use Getter and Setter methods, rather than a property.
public void SetExtendedData(JObject extendedData) {
ExtendedData = JsonConvert.SerializeObject(extendedData);
_deserializedExtendedData = extendedData;
}
//just to prevent deserializing more than once unnecessarily
private JObject _deserializedExtendedData;
public JObject GetExtendedData() {
if (_extendedData != null) return _deserializedExtendedData;
_deserializedExtendedData = string.IsNullOrEmpty(ExtendedData) ? null : JsonConvert.DeserializeObject<JObject>(ExtendedData);
return _deserializedExtendedData;
}
You could theoretically do this:
campaign.GetExtendedData().Add(something);
But it's much more clear that That Doesn't Do What You Think It Does™.
If you're using database-first and using some kind of class auto-generator for EF, then the classes will usually be declared as partial, so you can add this stuff in a separate file that won't get blown away the next time you update your classes from your database.

Categories

Resources