动态架构设计Skill dynamic-schema-design

这个技能用于使用EF Core的JSON列设计和实现动态内容模式,允许在不进行数据库迁移的情况下添加自定义字段,适用于内容管理系统(CMS)等场景。关键词包括EF Core、JSON列、动态模式、数据库迁移、CMS、内容管理、灵活字段存储、模式演化。

架构设计 0 次安装 0 次浏览 更新于 3/11/2026

名称:动态架构设计 描述:在使用EF Core JSON列实现灵活内容模式时使用,使用 OwnsOne().ToJson() 模式,或设计避免迁移的动态字段存储。涵盖JSON列配置、JSON属性的LINQ查询、索引策略,以及无头CMS架构的模式演化模式。 允许工具:阅读、全局搜索、查找、任务、技能

使用EF Core JSON列进行动态架构设计

使用EF Core JSON列实现灵活内容模式的指南,允许在不进行数据库迁移的情况下添加动态自定义字段。

何时使用此技能

  • 为CMS内容类型设计自定义字段存储
  • 实现每个内容实例变化的动态属性
  • 避免因模式更改而频繁进行数据库迁移
  • 在EF Core中使用LINQ查询JSON数据
  • 规划JSON列的索引策略
  • 从EAV(实体-属性-值)迁移到JSON存储

EF Core JSON列基础

基本配置(.NET 10 / EF Core 10)

// 包含JSON存储自定义字段的实体
public class ContentItem
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public DateTime CreatedUtc { get; set; }

    // 用于动态字段的JSON列
    public CustomFieldsData CustomFields { get; set; } = new();
}

// 作为JSON存储的拥有实体
public class CustomFieldsData
{
    public Dictionary<string, object?> Fields { get; set; } = new();
    public Dictionary<string, FieldMetadata> Metadata { get; set; } = new();
}

public class FieldMetadata
{
    public string FieldType { get; set; } = string.Empty;
    public bool IsRequired { get; set; }
    public string? DisplayName { get; set; }
}

DbContext配置

public class ContentDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ContentItem>(entity =>
        {
            entity.HasKey(e => e.Id);

            // 使用ToJson()配置JSON列
            entity.OwnsOne(e => e.CustomFields, builder =>
            {
                builder.ToJson();
            });
        });
    }
}

JSON列模式

模式1:类型化自定义字段

最适合在编译时已知字段模式的情况。

// 强类型自定义字段
public class ArticleFields
{
    public string? Subtitle { get; set; }
    public List<string> Tags { get; set; } = new();
    public AuthorInfo? Author { get; set; }
    public int? ReadTimeMinutes { get; set; }
    public bool IsFeatured { get; set; }
}

public class AuthorInfo
{
    public Guid AuthorId { get; set; }
    public string DisplayName { get; set; } = string.Empty;
    public string? Bio { get; set; }
}

// 使用类型化字段的实体
public class Article
{
    public Guid Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;

    public ArticleFields Fields { get; set; } = new();
}

// 配置
modelBuilder.Entity<Article>(entity =>
{
    entity.OwnsOne(e => e.Fields, builder =>
    {
        builder.ToJson();
        builder.OwnsOne(f => f.Author);
    });
});

模式2:动态属性包

最适合完全动态模式,其中字段随实例变化。

public class DynamicContent
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;

    // 灵活属性包
    public JsonDocument? Properties { get; set; }
}

// 使用字典的替代方法
public class FlexibleContent
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;

    public Dictionary<string, JsonElement> Fields { get; set; } = new();
}

模式3:混合方法(推荐)

将常见字段的固定列与扩展的JSON列结合。

public class ContentItem
{
    // 固定列(索引化,频繁查询)
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;
    public string? Slug { get; set; }
    public ContentStatus Status { get; set; }
    public DateTime CreatedUtc { get; set; }
    public DateTime? PublishedUtc { get; set; }

    // 用于类型特定和自定义字段的JSON列
    public ContentExtensions Extensions { get; set; } = new();
}

public class ContentExtensions
{
    // 作为嵌套JSON存储的部分特定数据
    public TitlePartData? TitlePart { get; set; }
    public SeoPartData? SeoPart { get; set; }
    public MediaPartData? MediaPart { get; set; }

    // 完全动态的自定义字段
    public Dictionary<string, object?> CustomFields { get; set; } = new();
}

查询JSON列

在JSON属性上的LINQ查询

// 查询嵌套JSON属性
var featuredArticles = await context.Articles
    .Where(a => a.Fields.IsFeatured == true)
    .ToListAsync();

// 查询嵌套对象属性
var articlesByAuthor = await context.Articles
    .Where(a => a.Fields.Author!.AuthorId == authorId)
    .ToListAsync();

// 查询数组包含
var taggedArticles = await context.Articles
    .Where(a => a.Fields.Tags.Contains("technology"))
    .ToListAsync();

// 按JSON属性排序
var orderedArticles = await context.Articles
    .OrderByDescending(a => a.Fields.ReadTimeMinutes)
    .ToListAsync();

复杂JSON查询的原始SQL

// SQL Server JSON_VALUE
var results = await context.ContentItems
    .FromSqlRaw(@"
        SELECT * FROM ContentItems
        WHERE JSON_VALUE(Extensions, '$.CustomFields.rating') > 4
    ")
    .ToListAsync();

// PostgreSQL jsonb运算符
var results = await context.ContentItems
    .FromSqlRaw(@"
        SELECT * FROM ""ContentItems""
        WHERE ""Extensions""->>'CustomFields'->>'category' = 'tech'
    ")
    .ToListAsync();

索引策略

为频繁查询的JSON属性使用计算列

-- SQL Server:添加计算列
ALTER TABLE ContentItems
ADD Status AS JSON_VALUE(Extensions, '$.status') PERSISTED;

-- 在计算列上创建索引
CREATE INDEX IX_ContentItems_Status ON ContentItems(Status);

PostgreSQL的GIN索引用于JSONB

-- 索引整个JSON列
CREATE INDEX IX_ContentItems_Extensions ON "ContentItems"
USING GIN ("Extensions");

-- 索引特定路径
CREATE INDEX IX_ContentItems_Tags ON "ContentItems"
USING GIN (("Extensions"->'CustomFields'->'tags'));

EF Core迁移用于计算列

migrationBuilder.Sql(@"
    ALTER TABLE ContentItems
    ADD ComputedStatus AS JSON_VALUE(Extensions, '$.SeoPart.noIndex') PERSISTED;

    CREATE INDEX IX_ContentItems_ComputedStatus
    ON ContentItems(ComputedStatus);
");

模式演化

添加新字段

无需迁移 - 只需更新类并序列化:

// 之前
public class ArticleFields
{
    public string? Subtitle { get; set; }
}

// 之后 - 无需迁移
public class ArticleFields
{
    public string? Subtitle { get; set; }
    public string? Summary { get; set; }  // 新字段
    public List<string> RelatedLinks { get; set; } = new();  // 新字段
}

处理缺失/空属性

// 使用带默认值的可空类型
public class ContentFields
{
    public string? OptionalField { get; set; }
    public int RequiredWithDefault { get; set; } = 0;
    public List<string> CollectionWithDefault { get; set; } = new();
}

// 带空处理的查询
var items = await context.ContentItems
    .Where(c => c.Extensions.CustomFields != null
             && c.Extensions.CustomFields.ContainsKey("rating"))
    .ToListAsync();

模式更改的数据迁移

// 后台作业以迁移现有数据
public async Task MigrateContentSchema(ContentDbContext context)
{
    var batchSize = 100;
    var skip = 0;

    while (true)
    {
        var items = await context.ContentItems
            .OrderBy(c => c.Id)
            .Skip(skip)
            .Take(batchSize)
            .ToListAsync();

        if (!items.Any()) break;

        foreach (var item in items)
        {
            // 将旧模式转换为新模式
            if (item.Extensions.CustomFields.TryGetValue("old_field", out var value))
            {
                item.Extensions.CustomFields["new_field"] = value;
                item.Extensions.CustomFields.Remove("old_field");
            }
        }

        await context.SaveChangesAsync();
        skip += batchSize;
    }
}

性能考虑

何时使用JSON列

场景 使用JSON列 使用常规列
频繁过滤/排序
很少查询,仅用于显示
随内容类型变化
在所有实例中固定
作为唯一约束的一部分
需要全文搜索 考虑

最佳实践

做:
- 使用混合方法(固定 + JSON)
- 索引频繁查询的JSON路径
- 当模式已知时使用类型化DTO
- 在应用层验证JSON结构
- 对大JSON数组使用分页

不做:
- 在JSON中存储大型二进制数据
- 用于外键关系
- 仅依赖JSON查询性能关键路径
- 存储深层嵌套层次(尽可能扁平化)
- 跳过用户提供数据的模式验证

序列化配置

System.Text.Json选项

services.AddDbContext<ContentDbContext>(options =>
{
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        // 配置JSON序列化
    });
});

// 自定义JsonSerializerOptions
public static class JsonDefaults
{
    public static JsonSerializerOptions ContentOptions { get; } = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters =
        {
            new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
        }
    };
}

自定义类型转换器

// 用于不能直接序列化的复杂类型
public class PolymorphicFieldConverter : JsonConverter<object>
{
    public override object? Read(ref Utf8JsonReader reader,
        Type typeToConvert, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.ParseValue(ref reader);
        var root = doc.RootElement;

        // 从鉴别器确定类型
        if (root.TryGetProperty("$type", out var typeElement))
        {
            var typeName = typeElement.GetString();
            // 解析并反序列化适当类型
        }

        return root.Clone();
    }

    public override void Write(Utf8JsonWriter writer,
        object value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

内容部分JSON存储

在单个JSON列中存储多个部分

public class ContentItem
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;

    // 所有部分存储在单个JSON列中
    public ContentParts Parts { get; set; } = new();
}

public class ContentParts
{
    public TitlePart? Title { get; set; }
    public BodyPart? Body { get; set; }
    public AutoroutePart? Autoroute { get; set; }
    public PublishLaterPart? PublishLater { get; set; }
    public SeoMetaPart? SeoMeta { get; set; }

    // 自定义部分的扩展点
    public Dictionary<string, JsonElement> CustomParts { get; set; } = new();
}

// 部分定义
public record TitlePart(string Title, string? DisplayTitle = null);
public record BodyPart(string Html, string? PlainText = null);
public record AutoroutePart(string Path, bool IsCustom = false);
public record PublishLaterPart(DateTime? ScheduledUtc, string? TimeZone = null);
public record SeoMetaPart(string? MetaTitle, string? MetaDescription, bool NoIndex = false);

验证模式

对JSON字段使用流畅验证

public class ContentItemValidator : AbstractValidator<ContentItem>
{
    public ContentItemValidator(IContentTypeRegistry typeRegistry)
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(200);

        RuleFor(x => x.Extensions)
            .SetValidator(new ContentExtensionsValidator(typeRegistry));
    }
}

public class ContentExtensionsValidator : AbstractValidator<ContentExtensions>
{
    public ContentExtensionsValidator(IContentTypeRegistry typeRegistry)
    {
        When(x => x.SeoPart != null, () =>
        {
            RuleFor(x => x.SeoPart!.MetaTitle)
                .MaximumLength(60)
                .When(x => x.SeoPart?.MetaTitle != null);

            RuleFor(x => x.SeoPart!.MetaDescription)
                .MaximumLength(160)
                .When(x => x.SeoPart?.MetaDescription != null);
        });
    }
}

相关技能

  • content-type-modeling - 内容类型层次和组合
  • content-versioning - 带有JSON快照的版本历史
  • headless-api-design - 基于JSON内容的API契约