Как хранить JSON в поле объекта с помощью EF Core?
Я создаю многоразовую библиотеку с использованием .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; }
}
и у меня есть класс DbContext, который определяет DbSet:
public DbSet<Campaign> Campaigns { get; set; }
(Я также использую шаблон репозитория с DI, но я не думаю, что это имеет значение.)
Мои модульные тесты дают мне эту ошибку:
System.InvalidOperationException: невозможно определить отношения представленный навигационным свойством "JToken.Parent" типа 'JContainer. Либо вручную настройте связь, либо проигнорируйте это свойство из модели.
Есть ли способ указать, что это не отношения, но следует хранить в виде большой строки?
Ответы
Ответ 1
@Майкл ответил мне на ходу, но я реализовал его немного по-другому. Я закончил тем, что сохранил значение как строку в частной собственности и использовал ее как "Backing Field". Свойство ExtendedData затем преобразует JObject в строку на множестве и наоборот: get:
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
в качестве поля поддержки, я добавил это в свой контекст:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Campaign>()
.Property<string>("ExtendedDataStr")
.HasField("_extendedData");
}
Обновление: Даррен отвечает за использование конверсий основных значений EF (новый для EF Core 2.1 - который не существовал на момент ответа), кажется, лучший способ пойти на этом этапе.
Ответ 2
Собираюсь ответить на это по-другому.
В идеале модель предметной области не должна иметь представления о том, как хранятся данные. Добавление [NotMapped]
полей и дополнительных свойств [NotMapped]
фактически [NotMapped]
вашу модель домена с вашей инфраструктурой.
Помните - ваш домен король, а не база данных. База данных просто используется для хранения частей вашего домена.
Вместо этого вы можете использовать метод EF Core HasConversion()
объекта EntityTypeBuilder
для преобразования между вашим типом и JSON.
Учитывая эти 2 модели предметной области:
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; }
}
Я только добавил атрибуты, которые интересуют домен, а не детали, которые могут заинтересовать БД; IE нет [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 }));
}
}
С помощью этого метода вы можете полностью отделить свой домен от вашей инфраструктуры. Нет необходимости во всех вспомогательных полях и дополнительных свойствах.
Ответ 3
Не могли бы вы попробовать что-то вроде этого?
[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),
Ответ 4
Для тех, кто использует EF 2.1, есть небольшой пакет Nufet 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();
}
}
Ответ 5
Комментарий @Métoule:
Будьте осторожны с этим подходом: EF Core помечает объект как измененный, только если поле назначено. Поэтому, если вы используете person.Addresses.Add, объект не будет помечен как обновленный; вам нужно вызвать установщик свойства person.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
, поэтому вы можете добавить этот материал в отдельный файл, который не будет снесен в следующий раз Вы обновляете свои классы из своей базы данных.