在玩 EF Core 7 JSON 列时,我试图达到我保存的 JSON 看起来像这样的地步:
{
"Courses": [
"123": {
"RemoteCourseId": 123,
"Delivery": {
"Method": "email",
"ModeTarget": "[email protected]",
"Time": "13:30:00"
},
"Quizzes": [
"54321": {
"RemoteQuizId": 54321,
"Credits": null,
"Details": {
"CompletedDate": null,
"StartDate": "2023-04-17T17:17:10.315684Z"
},
"Questions": [
"12345": {
"ExpirationDate": "2023-04-17T17:17:10.316138Z",
"IsCorrect": false,
"QuestionId": 12345,
"QuestionUrl": "https://stackoverflow.com",
"SentDate": "2023-04-17T17:17:10.316181Z"
}
]
}
]
}
]
}
我面临的挑战是如何获取标识符,换一种方式显示:
{
"Courses": [
"123": {},
"789": {}
]
}
我想达到可以指定“123”和“789”的地步。我的尝试是像这样使用
Dictionary
:
public class LearnerCustomDataEntity
{
public int Id { get; set; }
public int LtiUserId { get; set; }
public LearnerCustomDataDto Data { get; set; }
}
public class LearnerCustomDataDto
{
public LearnerCustomDataProfileDto Profile { get; set; }
public Dictionary<int, LearnerCustomDataCourseDto> Courses { get; set; }
}
在 DbContext 中,我指定 JSON 列 (
LearnerCustomDataEntity.Data
),如下所示:
modelBuilder.Entity<LearnerCustomDataEntity>()
.OwnsOne(l => l.Data, onb =>
{
onb.OwnsOne<LearnerCustomDataProfileDto>(lcde => lcde.Profile);
onb.OwnsOne(lcdc => lcdc.Courses, onb =>
{
// I'm not sure _how_ to configure this
onb.HasOne(l => l);
});
onb.ToJson();
});
我不知道如何为
LearnerCustomDataDto.Courses
字段配置OwnedNavigationBuilder。
我的最终目标是能够使用类似的东西获得特定课程
LearnerCustomDataCourseDto dto = dbContext.LearnerCustomData.Single(l => l.Data.Courses[123]);
在这里使用字典是正确的方法吗?我当然愿意接受有关使用字典或更改为其他内容的建议。
更新:我尝试使用 KeyedCollection 实现,因为它可以实现通过密钥访问的目标,但我在转换时遇到了问题。所以,例如:
public class LearnerCustomDataDto
{
public LearnerCustomDataProfileDto Profile { get; set; }
public LearnerCustomDataCourseDtoCollection Courses { get; set; }
}
public class LearnerCustomDataCourseDtoCollection : KeyedCollection<int, LearnerCustomDataCourseDto>
{
protected override int GetKeyForItem(LearnerCustomDataCourseDto item)
{
return item.RemoteCourseId;
}
}
上下文的更新使它看起来像这样:
modelBuilder.Entity<LearnerCustomDataEntity>()
.OwnsOne(l => l.Data, onb =>
{
onb.OwnsOne(lcde => lcde.Profile);
onb.OwnsMany(lcde => lcde.Courses);
onb.ToJson();
});
然而,当我试图删除一个实体时,我得到一个
InvalidOperationException
:
System.InvalidOperationException : The requested operation requires an element of type 'Array', but the target element has type 'Object'.
Stack Trace:
[xUnit.net 00:00:46.97] at System.Text.Json.ThrowHelper.ThrowJsonElementWrongTypeException(JsonTokenType expectedType, JsonTokenType actualType)
[xUnit.net 00:00:46.98] at System.Text.Json.JsonElement.EnumerateArray()
[xUnit.net 00:00:46.98] at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.IncludeJsonEntityCollection[TIncludingEntity,TIncludedCollectionElement](QueryContext queryContext, Nullable`1 jsonElement, Object[] keyPropertyValues, TIncludingEntity entity, Func`4 innerShaper, Action`2 fixup)
[xUnit.net 00:00:46.98] at lambda_method654(Closure, QueryContext, Object[], JsonElement)
[xUnit.net 00:00:46.98] at lambda_method653(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
[xUnit.net 00:00:46.98] at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
[xUnit.net 00:00:46.98] at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
[xUnit.net 00:00:46.98] at lambda_method670(Closure, QueryContext)
[xUnit.net 00:00:46.98] at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
[xUnit.net 00:00:46.98] at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
由以下因素触发:
_db.LearnerCustomData.SingleOrDefault(l => l.LtiUserId == defaultUser.Id);
最简单的方法是将 EF 导航属性与 JSON 字典分开。这是一个简单的技巧,应该适用于每个 ORM 和每个序列化程序。
class ParentEntity
{
[JsonIgnore]
[XmlIgnore] [CsvIgnore] [FaxToCIAIgnore] //future proofing!
public HashSet<ChildEntity> Children { get; set; }
[NotMapped] //don't include this field in EF black magic
public Dictionary<int, ChildEntity> JsonChildren => Children.ToDictionary(c=>c.Id, c=>c);
}
只需为漂亮的属性名称添加一些属性即可完成。
但是无论如何你都应该定义
ViewModel
/DTO
类,你仍然会在查询物化或getter属性中有ToDictionary
但在代码审查期间不会受到打击。
你的最终目标看起来就像通过两个 ID 选择实体,所以只需做简单的选择语句。