Как хранить 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, поэтому вы можете добавить этот материал в отдельный файл, который не будет снесен в следующий раз Вы обновляете свои классы из своей базы данных.