我正在使用.NET Core创建一个可重用的库(面向.NETStandard 1.4),我使用的是Entity Framework Core(两者都是新的)。我有一个看起来像的实体类:
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
我有一个定义DbSet的DbContext类:
public DbSet<Campaign> Campaigns { get; set; }
(我也在使用DI的Repository模式,但我认为这不相关。)
我的单元测试给了我这个错误:
System.InvalidOperationException:无法确定关系 以导航属性为代表的JToken.Parent&#39;类型 &#39; JContainer&#39 ;.手动配置关系,或忽略 这个属性来自模特..
有没有办法表明这不是关系,但应该存储为大字符串?
答案 0 :(得分:54)
要以不同的方式回答这个问题。
理想情况下,域模型应该不知道如何存储数据。添加支持字段和额外的[NotMapped]
属性实际上是将域模型耦合到基础架构。
请记住-您的域是国王,而不是数据库。该数据库仅用于存储您的域的一部分。
相反,您可以在HasConversion()
对象上使用EF Core的EntityTypeBuilder
方法在类型和JSON之间进行转换。
给出以下两个领域模型:
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; }
}
我仅添加了域感兴趣的属性-并没有添加数据库感兴趣的详细信息;即没有[Key]
。
我的DbContext对于IEntityTypeConfiguration
具有以下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 }));
}
}
使用此方法,您可以完全将您的域与基础架构分离。不需要所有的支持字段和额外的属性。
答案 1 :(得分:15)
@ Michael的回答让我走上了正轨,但我的实施方式略有不同。我最终将值作为字符串存储在私有属性中,并将其用作&#34; Backing Field&#34;。然后,ExtendedData属性将JObject转换为set上的字符串,反之亦然:get:
id
message
sender
chat_id
要将public class Campaign
{
// https://docs.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();
}
}
}
设置为支持字段,我将其添加到我的上下文中:
_extendedData
更新:Darren回答使用EF Core Value Conversions(EF Core 2.1的新功能 - 在此答案时尚未存在)似乎是目前最好的方法。
答案 2 :(得分:4)
你能尝试这样的事吗?
[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);
}
}
这是迁移输出:
ExtraData = table.Column<string>(nullable: true),
答案 3 :(得分:2)
正确执行Change Tracker功能的关键是实现ValueComparer和ValueConverter。下面是实现这种扩展的方法:
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;
}
}
这是如何工作的示例。
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>>();
}
}
这将使ChangeTracker正常运行。
答案 4 :(得分:2)
// DbContext
// This is our model
class Task {
var name = ""
var checked = false
var date = Date()
var category: String
var number: Int
var itemID: String?
public init() {
self.category = ""
self.number = 0
}
}
// Extension for init from firebase response
extension Task {
convenience init(with firebase: [String: Any]) {
self.init()
self.name = (firebase["name"] as? String) ?? ""
}
}
// We create service for document
// We use this service like an API
final class DocumentService {
static let shared = DocumentService()
private let database: FirebaseDatabase
private var tasks: [[Task]] = []
public init(database: FirebaseDatabase = FirebaseDatabase()) {
self.database = database
}
func load(in section: Int, completion: @escaping (([Task]) -> Void)) {
database.loadData(section: section) { [unowned self] tasks in
self.tasks[section] = tasks.map(Task.init)
completion(self.tasks[section])
}
}
func check(at indexPath: IndexPath, isChecked: Bool) {
tasks[indexPath.section][indexPath.row].checked = isChecked
}
}
// We create firebase database class we can add some features in here
final class FirebaseDatabase {
func loadData(section: Int, completion: @escaping (([[String: Any]]) -> Void)) {
// TODO: firebase load data
let response: [[String: Any]] = [
["name": "Stackoverflow"]
]
completion(response)
}
}
final class TestController: UIViewController {
private let service = DocumentService.shared
override func viewDidLoad() {
super.viewDidLoad()
service.load(in: 0) { tasks in
// TODO
}
}
}
创建一个属性来处理实体的属性。
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);
}
创建扩展类以查找Josn属性
public class HasJsonConversionAttribute : System.Attribute
{
}
实体示例
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;
}
}
答案 5 :(得分:1)
对于使用EF 2.1的用户,有一个不错的NuGet小包EfCoreJsonValueConverter,它非常简单。
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();
}
}
答案 6 :(得分:1)
这是我用过的东西
模型
public class FacilityModel
{
public string Name { get; set; }
public JObject Values { get; set; }
}
实体
[Table("facility", Schema = "public")]
public class Facility
{
public string Name { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
}
映射
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));
答案 7 :(得分:0)
请谨慎使用此方法:仅当字段分配给时,EF Core才会将实体标记为已修改。因此,如果您使用person.Addresses.Add,则该实体不会被标记为已更新;您需要致电属性设置器人员。Addresses= UpdatedAddresses。
让我采取了另一种方法,以便使这一事实显而易见:使用Getter和Setter 方法,而不是使用属性。
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;
}
从理论上讲,您可以这样做:
campaign.GetExtendedData().Add(something);
但是,更明显的是,它不会按照您认为的那样做™。
如果您先使用数据库,然后使用某种用于EF的类自动生成器,则这些类通常将声明为partial
,因此您可以将此内容添加到一个单独的文件中,而不会下次您从数据库更新课程时,就不会感到惊讶。
答案 8 :(得分:0)
对于使用EF Core 3.1并遇到此类错误的开发人员(“实体类型'XXX'需要定义主键。如果您打算使用无密钥实体类型调用'HasNoKey()'。”)解决方案只是将lambda从.HasConversion()方法中移出: 公共类OrderConfiguration:IEntityTypeConfiguration可以: 受保护的重写void OnModelCreating(ModelBuilder modelBuilder)//在YourModelContext中:DbContext类。
答案 9 :(得分:0)
我根据 Robert Raboud's 的贡献提出了一个解决方案。我所做的改变是我的实现使用了依赖 System.Text.Json 包而不是 Newtonsofts 库的 HasJsonConversion 方法:
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;
}
另请注意,由于我使用的是 MySQL 设置,因此此实现要求列为 LONGTEXT。