JSON反序列化由于缺少属性而崩溃。

huangapple go评论67阅读模式
英文:

JSON Deserialize is crashing due to missing properties

问题

I am working in Blazor server app (.Net v6). I have an object that I am serializing and storing in the session to be retrieved by another page. I started off with a straight-up Storage.SetAsync("request", myObject).ConfigureAwait(false);. That failed for a Self referencing loop detected error. So I converted it prior to saving in the session so I can set ReferenceLoopHanding.Ignore.

var requestToJson = JsonConvert.SerializeObject(_request, Formatting.None,
    new JsonSerializerSettings()
    {
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    });

await Storage.SetAsync("request", requestToJson).ConfigureAwait(false);

The serialization completes without error. But when I try and deserialize, the app crashes with an "access violation" error.

After some searching I found the json validation tools and my deserialize code looks like this

var result = await Storage.GetAsync<string>("request").ConfigureAwait(false);
var json = result.Value;

JSchemaGenerator generator = new JSchemaGenerator();
JSchema schemaGenerator = generator.Generate(typeof(RequestDetailDTO));

JObject detail = JObject.Parse(json);
IList<string> errors;
bool valid = detail.IsValid(schemaGenerator, out errors);
var jsonToRequest = JsonConvert.DeserializeObject<RequestDetailDTO>(json);

And I was able to capture the errors:

[![enter image description here][1]][1]

Here is the object I'm serializing

public class RequestDetailDTO
{
    public int RequestId { get; set; }
    public string Justification { get; set; } = string.Empty;
    public string SubmitUser { get; set; } = string.Empty;
    public DateTime SubmitDate { get; set; }
    public string? ModifyUser { get; set; } = string.Empty;
    public DateTime? ModifyDate { get; set; }
    [JsonIgnore]
    public string CurrentStatus 
    {
        get
        {
            if (Statuses.Any())
            {
                return Statuses.MaxBy(x => x.RequestStatusTypeId).RequestStatusType.RequestStatusTypeName;
            }
            return string.Empty;
        }
        set => CurrentStatus = value; 
    }
    public virtual List<ChangeDetailDTO> ChangeDetail { get; set; } = new List<ChangeDetailDTO>();
    public virtual List<Status> Statuses { get; set; } = new List<Status>();
    public int RequestTypeId { get; set; }
    public virtual RequestType RequestType { get; set; } = new RequestType();
}

Here is the output of the serialization action.

{
   "RequestId":12507,
   "Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery  escalations relating to asset returns and exit interview support.",
   "SubmitUser":"Ethel Jones",
   "SubmitDate":"2023-05-26T00:00:00",
   "ModifyUser":null,
   "ModifyDate":null,
   "CurrentStatus":"Approved",
   "ChangeDetail":[ + ],
   "Statuses":[
      {
         "StatusId":1279,
         "RequestId":12507,
         "SubmitUser":" \r",
         "StatusDate":"2023-05-26T00:00:00",
         "Requests":[
            {
               "RequestId":12507,
               "Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery  escalations relating to asset returns and exit interview support.",
               "SubmitUser":"Ethel Jones",
               "SubmitDate":"2023-05-26T00:00:00",
               "ModifyUser":null,
               "ModifyDate":null,
               "ChangeDetail":[
                  {
                     "ChangeDetailId":2442,
                     "RequestId":12507,
                     "CurrentL1":"HR Consulting",
                     "CurrentL2":"Movements",
                     "CurrentL3":"Voluntary Exit Management",
                     "CurrentL4":"Employee Resignation Management",
                     "CurrentL5":null,
                     "NewL1":"HR Consulting",
                     "NewL2":"Movements",
                     "NewL3":"Voluntary Exit Management",
                     "NewL4":"Employee Resignation Management",
                     "NewL5":"Exit Interview Support",
                     "SubmitUser":"Ethel Jones\r",
                     "SubmitDate":"2023-05-26T00:00:00",
                     "ModifyUser":"",
                     "ModifyDate":null
                  }
               ],
               "Statuses":[
                  
               ],
               "RequestTypeId":1,
               "RequestType":{
                  "RequestTypeId":1,
                  "RequestTypeName":"New",
                  "Requests":[
                     
                  ]
               }
            }
         ],
         "RequestStatusTypeId":4,
         "RequestStatusType":{
            "RequestStatusTypeId":4,
            "RequestStatusTypeName":"Approved",
            "Statuses":[
               
            ]
         }
      }
   ],
   "RequestTypeId":0,
   "RequestType":{
      "RequestTypeId":1,
      "RequestTypeName":"New",
      "Requests":[
         {
            "RequestId":12507,
            "Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery  escalations relating to asset returns and exit interview support.",
            "SubmitUser":"Ethel Jones",
            "SubmitDate":"2023-05-26T00:00:00",
            "ModifyUser":null,
            "ModifyDate":null,
            "ChangeDetail":[
               {
                  "ChangeDetailId":2442,
                  "RequestId":12507,
                  "CurrentL1":"HR Consulting",
                  "CurrentL2":"Movements",
                  "CurrentL3":"Voluntary Exit Management",
                  "CurrentL4":"Employee Resignation Management",
                  "CurrentL5":null,
                  "NewL1":"HR Consulting",
                  "NewL2":"Movements",
                  "NewL3":"Voluntary Exit Management",
                  "NewL4":"Employee Resignation Management",
                  "NewL5":"Exit Interview Support",
                  "SubmitUser":"Ethel Jones\r",
                  "SubmitDate":"2023-05-26T00:00:00",
                  "ModifyUser":"",
                  "ModifyDate":null
               }
            ],
            "Statuses":[
               {
                  "StatusId":1279,
                  "RequestId":12507,
                  "SubmitUser":" \r",
                  "StatusDate":"2023-05-26T00:00:00",
                  "Requests":[
                     
                  ],
                  "RequestStatusTypeId":4,
                  "RequestStatusType":{
                     "RequestStatusTypeId":4,
                     "RequestStatusTypeName":"Approved",
                     "Statuses":[
                        
                     ]
                  }
               }
            ],
            "RequestTypeId":1
         }
      ]
   }
}

Here is the output of the deserialize action. It seems to be dropping data, which accounts for the errors.

{
   "RequestId":12507,
   "Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios:

<details>
<summary>英文:</summary>

I am working in Blazor server app (.Net v6). I have an object that I am serializing and storing in the session to be retrieved by another page.  I started off with a straight-up `Storage.SetAsync(&quot;request&quot;, myObject).ConfigureAwait(false);`.  That failed for a `Self referencing loop detected` error.  So I converted it prior to saving in the session so I can set `ReferenceLoopHanding.Ignore`.

var requestToJson = JsonConvert.SerializeObject(_request, Formatting.None,
new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});

await Storage.SetAsync("request", requestToJson).ConfigureAwait(false);


The serialization completes without error.  But when I try and deserialize, the app crashes with an &quot;access violation&quot; error.
After some searching I found the json validation tools and my deserialize code looks like this

var result = await Storage.GetAsync<string>("request").ConfigureAwait(false);
var json = result.Value;

JSchemaGenerator generator = new JSchemaGenerator();
JSchema schemaGenerator = generator.Generate(typeof(RequestDetailDTO));

JObject detail = JObject.Parse(json);
IList<string> errors;
bool valid = detail.IsValid(schemaGenerator, out errors);
var jsonToRequest = JsonConvert.DeserializeObject<RequestDetailDTO>(json);


And I was able to capture the errors 
[![enter image description here][1]][1]
Here is the object I&#39;m serializing
public class RequestDetailDTO
{
public int RequestId { get; set; }
public string Justification { get; set; } = string.Empty;
public string SubmitUser { get; set; } = string.Empty;
public DateTime SubmitDate { get; set; }
public string? ModifyUser { get; set; } = string.Empty;
public DateTime? ModifyDate { get; set; }
[JsonIgnore]
public string CurrentStatus 
{
get
{
if (Statuses.Any())
{
return Statuses.MaxBy(x =&gt; x.RequestStatusTypeId).RequestStatusType.RequestStatusTypeName;
}
return string.Empty;
}
set =&gt; CurrentStatus = value; 
}
public virtual List&lt;ChangeDetailDTO&gt; ChangeDetail { get; set; } = new List&lt;ChangeDetailDTO&gt;();
public virtual List&lt;Status&gt; Statuses { get; set; } = new List&lt;Status&gt;();
public int RequestTypeId { get; set; }
public virtual RequestType RequestType { get; set; } = new RequestType();
}

Here is the output of the serialization action.

{
"RequestId":12507,
"Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery escalations relating to asset returns and exit interview support.",
"SubmitUser":"Ethel Jones",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":null,
"ModifyDate":null,
"CurrentStatus":"Approved",
"ChangeDetail":[ + ],
"Statuses":[
{
"StatusId":1279,
"RequestId":12507,
"SubmitUser":" \r",
"StatusDate":"2023-05-26T00:00:00",
"Requests":[
{
"RequestId":12507,
"Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery escalations relating to asset returns and exit interview support.",
"SubmitUser":"Ethel Jones",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":null,
"ModifyDate":null,
"ChangeDetail":[
{
"ChangeDetailId":2442,
"RequestId":12507,
"CurrentL1":"HR Consulting",
"CurrentL2":"Movements",
"CurrentL3":"Voluntary Exit Management",
"CurrentL4":"Employee Resignation Management",
"CurrentL5":null,
"NewL1":"HR Consulting",
"NewL2":"Movements",
"NewL3":"Voluntary Exit Management",
"NewL4":"Employee Resignation Management",
"NewL5":"Exit Interview Support",
"SubmitUser":"Ethel Jones\r",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":"",
"ModifyDate":null
}
],
"Statuses":[

           ],
&quot;RequestTypeId&quot;:1,
&quot;RequestType&quot;:{
&quot;RequestTypeId&quot;:1,
&quot;RequestTypeName&quot;:&quot;New&quot;,
&quot;Requests&quot;:[
]
}
}
],
&quot;RequestStatusTypeId&quot;:4,
&quot;RequestStatusType&quot;:{
&quot;RequestStatusTypeId&quot;:4,
&quot;RequestStatusTypeName&quot;:&quot;Approved&quot;,
&quot;Statuses&quot;:[
]
}
}

],
"RequestTypeId":0,
"RequestType":{
"RequestTypeId":1,
"RequestTypeName":"New",
"Requests":[
{
"RequestId":12507,
"Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery escalations relating to asset returns and exit interview support.",
"SubmitUser":"Ethel Jones",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":null,
"ModifyDate":null,
"ChangeDetail":[
{
"ChangeDetailId":2442,
"RequestId":12507,
"CurrentL1":"HR Consulting",
"CurrentL2":"Movements",
"CurrentL3":"Voluntary Exit Management",
"CurrentL4":"Employee Resignation Management",
"CurrentL5":null,
"NewL1":"HR Consulting",
"NewL2":"Movements",
"NewL3":"Voluntary Exit Management",
"NewL4":"Employee Resignation Management",
"NewL5":"Exit Interview Support",
"SubmitUser":"Ethel Jones\r",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":"",
"ModifyDate":null
}
],
"Statuses":[
{
"StatusId":1279,
"RequestId":12507,
"SubmitUser":" \r",
"StatusDate":"2023-05-26T00:00:00",
"Requests":[

              ],
&quot;RequestStatusTypeId&quot;:4,
&quot;RequestStatusType&quot;:{
&quot;RequestStatusTypeId&quot;:4,
&quot;RequestStatusTypeName&quot;:&quot;Approved&quot;,
&quot;Statuses&quot;:[
]
}
}
],
&quot;RequestTypeId&quot;:1
}
]

}
}


Here is the output of the deserialize action.  It seems to be dropping data, which accounts for the errors.

{
"RequestId":12507,
"Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery escalations relating to asset returns and exit interview support.",
"SubmitUser":"Ethel Jones",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":null,
"ModifyDate":null,
"CurrentStatus":"Approved",
"ChangeDetail":[
],
"Statuses":[
{
"StatusId":1279,
"RequestId":12507,
"SubmitUser":" \r",
"StatusDate":"2023-05-26T00:00:00",
"Requests":[
{
"RequestId":12507,
"Justification":"Adding 3 additional L5 categories under Employee Resignation Management so teams can more easily track the volumes of three core scenarios: bonus recovery escalations relating to asset returns and exit interview support.",
"SubmitUser":"Ethel Jones",
"SubmitDate":"2023-05-26T00:00:00",
"ModifyUser":null,
"ModifyDate":null,
"ChangeDetail":[
{
I think this is the first error**
}
],
"Statuses":[
],
"RequestTypeId":1,
"RequestType":{
*I this this is the second and third errors
}
}
],
"RequestStatusTypeId":4,
"RequestStatusType":{
}
}
],
"RequestTypeId":0,
"RequestType":{
}
}


Sorry for all the JSON pasting, but I think its the Reference Loop Handling that is causing the missing data.  In my Request model, it has the Status object in it, which has the Request object in it, and so on; hence the Reference Loops.
Why is that, you ask?  Its because of the way I created my models in the db context. I&#39;m still pretty new to writing these so I may have made a grievous error.  
// this is a 1:N - Requests and RequestType
modelBuilder.Entity&lt;Request&gt;()
.HasOne(x =&gt; x.RequestType)
.WithMany(x =&gt; x.Requests);
// this is a 1:N - Requests and ChangeDetail
modelBuilder.Entity&lt;Request&gt;()
.HasMany(x =&gt; x.ChangeDetail)
.WithOne(x =&gt; x.Request);
// this is a N:N - Requests and Status
modelBuilder.Entity&lt;Request&gt;()
.HasMany(x =&gt; x.Statuses)
.WithMany(x =&gt; x.Requests);
// this is a 1:N - Status and RequestStatusType
modelBuilder.Entity&lt;Status&gt;()
.HasOne(x =&gt; x.RequestStatusType)
.WithMany(x =&gt; x.Statuses);

[1]: https://i.stack.imgur.com/APmxJ.png
</details>
# 答案1
**得分**: 3
你遇到的问题的关键是你将实体(Entities)和数据传输对象(DTOs)混淆了。数据传输对象(DTO),或称数据传输对象,应该是一个简单的POCO(普通的C#对象),其唯一目的是在各层/系统之间传递信息。实体是EF用来表达数据/领域状态的。这意味着DTO的关注点仅仅是你想在各层和系统之间发送和返回的数据的“形状”,而实体反映了系统内部领域的数据状态。实体的关注点是数据库中的数据结构。
所以在你的实体定义中,你在RequestDetail和Status之间有双向引用。由于Status包含RequestDetail的集合,你首先遇到了循环警告,因为一个请求将序列化状态,这将序列化请求,这将序列化状态...... 你可以阻止序列化陷入循环,但当你告诉反序列化器重新填充那些期望这些集合的实体时,它会失败,因为它无法填充它们。
解决方案是区分可以具有双向引用的实体和适合序列化的DTO。简而言之,实体的关注点是与数据库交互时的形状和关系。DTO的关注点是JSON的形状。所以在你的情况下,你当前的“DTO”对象应该被重新命名以去掉“DTO”,以充当具有当前关系的实体。然后根据你想要的JSON创建DTO类。DTO的关键区别在于这些类不会与它们的父关联双向引用。它们还可以精简,只包含在JSON中所需的数据,而不必使用`[JsonIgnore]`。从实体填充这些DTO被称为投影,可以使用`.Select()`或使用Automapper的`ProjectTo<TDTO>()`方法来完成。建议使用`ProjectTo`而不是使用`.Map()`,因为它可以与`IQueryable`一起工作,以与`Select()`相同的方式来形状SQL查询。
另一个注意事项是,当涉及到实体时,你应该初始化集合/集,但*不应*初始化单一引用。所以这是好的:
```csharp
public virtual List<ChangeDetail> ChangeDetail { get; set; } = new List<ChangeDetail>();

可以说更好的做法是将setter设置为protected并将名称变为“ChangeDetails”。除此初始化之外的代码不应该将这个集合设置为新的List<T>,包括如果你想要删除所有实体时也不应该这样做。

但不要这样做:

public virtual RequestType RequestType { get; set; } = new RequestType();

现在不幸的是,.Net Core将通过未设置非可空引用来投诉这一点。我们希望EF通过Required vs. Optional来管理这一点,但在实体看来,我们希望它是可为空的,因为在加载这个实体时,我们可能不想总是加载一个RequestType,并且EF可以通过延迟加载来保护这一点作为一个安全措施。有几种方法可以让智能感应不再警告,包括:

public virtual RequestType RequestType { get; set; } = default!; // 或者 null!

我认为你也可以在可为空属性上使用[Required]属性:

[Required]
public virtual RequestType? RequestType { get; set; }

就个人而言,由于实体可能会有一些不可为空的字段,并且我不想忘记这些个别设置并开始忽略警告,所以我倾向于只标记一个受保护的默认构造函数,以使其停止警告所有这些问题。 JSON反序列化由于缺少属性而崩溃。

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Constructor used by EF.
/// </summary>
protected RequestDetail()
{ }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

这个构造函数在你想要使用构造函数来使用参数初始化新的实体实例时也是有用的,因为EF无论如何都需要一个默认构造函数可用。

希望这能帮助你解决你所遇到的序列化和反序列化问题。

英文:

The crux of the issue you are encountering is that you are confusing Entities with DTOs. A DTO, or Data Transfer Object, should be a simple POCO (Plain old C# object) who's sole purpose is to relay information between layers/systems. Entities are what EF will use to express data/domain state. This means that a DTO's concern is solely the "shape" of data you want to send and return between layers and systems while an Entity reflects data state behind the scenes. An Entity's concern is the shape of the underlying domain for your system. Ultimately, the data structure in the database.

So in your entity definitions you have bi-directional references between your RequestDetail and your Status. Since a Status contains a collection of RequestDetails you first encountered a cycle warning as a request would serialize statuses which would serialize requests which would serialize statuses....... You can stop the serialization from getting caught in that loop, but when you tell the de-serializer to re-saturate your entities that are expecting those collections, it is failing because it cannot populate them.

The solution is to differentiate entities which can have bi-directional references, with DTOs suited to being serialized. To put it simply, The Entity's concern is the shape and relations when interacting with the database. The DTO's concern is the shape of the JSON. So in your case your current "DTO" objects should be relabeled to remove the "DTO" to serve as entities with the current relationships. Then create DTO classes based on the JSON you want. The key difference of the DTO is that these classes will not have bi-directional references back to their parent associations. They can also be streamlined to contain only the data that is needed in the JSON rather than having to use [JsonIgnore]. Populating these DTOs from entities is referred to as Projection and can be done using .Select() or with Automapper and it's ProjectTo&lt;TDTO&gt;() method. ProjectTo is recommended rather than using .Map() as it works with IQueryable to shape the SQL query in the same way that Select() does.

One other note is that when it comes to entities, you should initialize collections/sets, but not singular references. So this is good:

public virtual List&lt;ChangeDetail&gt; ChangeDetail { get; set; } = new List&lt;ChangeDetail&gt;();

Arguably better to make the setter protected and pluralize the name to "ChangeDetails". No code other than this initialization should ever set this collection to a new List&lt;T&gt; including if you want to remove all entities.

... but don't do this:

public virtual RequestType RequestType { get; set; } = new RequestType();

Now unfortunately .Net Core will now complain about this via the non-nullable reference not being set. We want EF to manage this via Required vs. Optional but we do want it to be Null-able so far as the entity is concerned as when loading this entity we may not want to always load a RequestType and EF can guard this with Lazy Loading as a failsafe. There are a few options to get Intellisense to shut up about the warning including:

public virtual RequestType RequestType { get; set; } = default!; // or null!

I believe you could also use the [Required] attribute with a null-able property:

[Required]
public virtual RequestType? RequestType { get; set; }

Personally since entities will likely have a few non-nullable fields in entities and I don't want to forget and of these individual settings and start ignoring warnings, I tend to just mark a protected default constructor to get it to shut up about all of them. JSON反序列化由于缺少属性而崩溃。

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// &lt;summary&gt;
/// Constructor used by EF.
/// &lt;/summary&gt;
protected RequestDetail()
{ }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

This constructor is doubly useful if you want to use constructors to initialize new entity instances with arguments as EF will need a default constructor available anyways.

That should hopefully get a handle on the serialization and deserialization issues you are seeing.

huangapple
  • 本文由 发表于 2023年7月14日 04:17:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76682972.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定