【问题标题】:Newtonsoft.JSON Serialize\Deserialize Datatable with GUIDNewtonsoft.JSON Serialize\Deserialize Datatable with GUID
【发布时间】:2017-08-24 14:53:49
【问题描述】:

我有一个用于处理各种数据库的 Web API 解决方案。一切正常,直到我访问了一个带有 GUID 号的表。我终于将问题缩小到序列化/反序列化方法。当 DataTable 被序列化为 JSON,然后随后反序列化回 DataTable 时,包含 GUID 的列的数据类型变为System.String,而不是原来的System.Guid

举个简单的例子:

System.Data.DataTable myData = new DataTable();
myData.Columns.Add("ID", typeof(System.Guid));
myData.Columns.Add("Name", typeof(System.String));
myData.Rows.Add(System.Guid.NewGuid(), "Name1");

// Reports: System.Guid
System.Diagnostics.Debug.Write(myData.Columns["ID"].DataType.FullName);

string mySerializedString = Newtonsoft.Json.JsonConvert.SerializeObject(myData);
myData = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Data.DataTable>(mySerializedString);

// Reports: System.String
System.Diagnostics.Debug.Write(myData.Columns["ID"].DataType.FullName);

这是一个通用 API,因此我不知道传递的表是否包含包含 GUID 或任何其他可能导致 JSON 问题的数据类型的列。

希望这只是因为我错过了一些愚蠢的东西,而这只是一个简单的修复?

**** 更新****

感谢所有提示。与此同时,我又遇到了另一个问题,可能这一切都结束了。

当您最初从数据库加载表时,每个行状态 = 不变。然而,通过序列化过程,结果是每个行状态都变为“已添加”。

以我查询 1 条记录为例,修改该记录并将其发回。 DataTable 现在已将该记录标记为“已添加”,这会破坏主键约束并炸毁。

这太糟糕了!

我基本上是在两端查看某种巨大的拼凑工作,我必须传递一个字典,其中包含我发现通过 JSON 流程处理的所有属性,然后在另一端全部重建。

一直在尝试通用并且不知道我应该处理什么样的数据。

&*(^% 它!

【问题讨论】:

  • 您可以使用this answer 来防止丢失的列类型。

标签: json serialization asp.net-web-api json.net


【解决方案1】:

在 JSON 中无法指示某事物是 GUID。
来自json.org

值可以是双引号中的字符串、数字、true 或 false 或 null,或对象或数组。这些结构可以 嵌套。

因为您没有反序列化回具有已定义类型的东西,所以反序列化程序无法知道它应该尝试将“GUID 作为字符串”转换回实际的 GUID。

如果您要反序列化回具有 GUID 类型的属性的对象,它会尝试将字符串解析为该属性的 GUID,但数据表的结构不是传输的 JSON 的一部分。

【讨论】:

    【解决方案2】:

    正如@Craig H 所指出的,JSON 的类型系统非常有限——它可以区分数字、布尔值、字符串、对象和数组,以及 null 值。如果您需要比这更具体的内容,则必须求助于将类型信息作为元数据嵌入 JSON 本身。

    Json.Net 有一个TypeNameHandling 设置,可用于将类型信息写入 JSON。不幸的是,Json.Net 附带的DataTableConverter 似乎不支持此设置。要解决此问题,您可以为 DataTables 使用自定义 JsonConverter,例如 How to include column metadata in JSON for an empty DataTable 中的那个

    像这样使用它:

    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.Converters.Add(new CustomDataTableConverter());
    
    string mySerializedString = JsonConvert.SerializeObject(myData, settings);
    myData = JsonConvert.DeserializeObject<DataTable>(mySerializedString, settings);
    

    这是一个包含您的示例代码的演示:https://dotnetfiddle.net/wXNy9o

    【讨论】:

      【解决方案3】:

      感谢大家的提示。在处理 JSON 和 .NET 数据表时,我必须克服许多不同的挑战。

      1. 返回 0 行的表不会序列化,因此您最终会得到一个包含 0 列的 Datatable。
      2. JSON 丢失列的数据类型
      3. JSON 丢失记录的 RowState。
      4. JSON 丢失了已修改行的 Original\Proposed 值,因此无法保存
      5. JSON 将 DataType Byte[] 转换为 Base64 字符串,但不会将其转换回来。

      因此,我找到了一个似乎可以一次性克服所有这些问题的解决方案。总结一下解决方案..

      • 使用 DataTable 的内置 XML 功能恢复架构
      • 保存原始 RowState 以便将其恢复
      • 保存修改记录的原始值以将其恢复。

      既然我们都喜欢代码...

      即将被序列化的类(还有更多属性,但这些是重点)

          public System.Data.DataTable Data { get; set; }
      
          [Newtonsoft.Json.JsonProperty]
          private string _DataTableSchema { get; set; }
      
          [Newtonsoft.Json.JsonProperty]
          private List<DeconstructedDataRow> _DeconstructedDataRows { get; set; }
      
          private class DeconstructedDataRow
          {
              public System.Data.DataRowState RowState { get; set; }
              public List<object> RowOriginalValues { get; set; }
      
              public DeconstructedDataRow()
              {
                  RowState = System.Data.DataRowState.Unchanged;
                  RowOriginalValues = new List<object>();
              }
          }
      

      从一方面来说,在序列化之前您调用的表 .Data_Deconstruct....

          public void Data_Deconstruct()
          {  
              //Couple of Pre-Checks
              _DataTableSchema = String.Empty;
              _DeconstructedDataRows.Clear(); 
              if (this.Data == null || this.Data.Columns.Count == 0)
              {
                  return;
              }
      
              //We need to mess around a bit so instead of tampering with their original table, we work with a copy
              System.Data.DataTable myWorkingData = this.Data.Copy();
      
      
              //In order to serialize to XML, the table MUST have a name
              if (String.IsNullOrWhiteSpace(myWorkingData.TableName) == true)
              {
                  myWorkingData.TableName = System.Guid.NewGuid().ToString();
              }
      
              //JSON doesn't carry over the Schema of the Table so we loose the specific 
              //DataTypes of each colum. So we use the Built-in method of the DataTable object to create 
              //a XML\String version of its schema 
              System.IO.StringWriter myStringWriter = new System.IO.StringWriter();
              myWorkingData.WriteXmlSchema(myStringWriter, true);
              _DataTableSchema = myStringWriter.ToString();
      
      
              //JSON and the process of Serializing and Deserializing doesn't carry over 
              //the proper RowState for each Record. In addition, for those records that
              //have been Modified, we lose the Original values, and for those records that 
              //have been Deleted, we can't serialize them. So this is a KLUDGE king that
              //seems to sort all this out.
              for (Int32 intRowIndex = 0; intRowIndex < myWorkingData.Rows.Count; intRowIndex++)
              {
                  DeconstructedDataRow myDeconstructedDataRow = new DeconstructedDataRow();
      
                  //So start by saving off the current RowState
                  myDeconstructedDataRow.RowState = myWorkingData.Rows[intRowIndex].RowState;
      
                  //If the RowState is DELETED, then the ORGINAL Record will not serialize, 
                  //so we need to reject the ORIGINAL state for now and we will restore it later
                  if (myDeconstructedDataRow.RowState == System.Data.DataRowState.Deleted)
                  {
                      this.Data.Rows[intRowIndex].RejectChanges();
                  }
      
                  //If the RowState is MODIFIED, then we have to restore the ORIGINAL values
                  //when we restore this record. Without the Original Values, the record won't 
                  //update and even if we force it, it will error out because of 'concurrency' errors.
                  if (myDeconstructedDataRow.RowState == System.Data.DataRowState.Modified)
                  {
                      myWorkingData.Rows[intRowIndex].RejectChanges();      
                      myDeconstructedDataRow.RowOriginalValues.AddRange(myWorkingData.Rows[intRowIndex].ItemArray);
                  }
      
                  //And don't forget to add it to our list
                  this._DeconstructedDataRows.Add(myDeconstructedDataRow);
              }
      
              //Clean up our Clone.
              myWorkingData.Dispose();
              myWorkingData = null;
          }
      

      另一方面,反序列化后你调用 .Data_Reconstruct....

          public void Data_Reconstruct()
          {
              //Couple of Pre-Checks
              if (this.Data == null || String.IsNullOrWhiteSpace(_DataTableSchema) == true)
              {
                  return;
              }
      
      
              //So first we build a new DataTable with the correct Schema
              System.Data.DataTable myWorkingData = new System.Data.DataTable();
              System.IO.StringReader myStringReader = new System.IO.StringReader(_DataTableSchema);
              myWorkingData.ReadXmlSchema(myStringReader);
      
      
              //Now we transfer over all the data that was serialize 'as-is' from the existing to the new Table
              foreach (System.Data.DataRow myRow in this.Data.Rows)
              {
                  //myWorkingData.ImportRow(myRow);  //Should have been this easy BUT ...
      
                  // JSON converts some data types to a different format, but then when it deserializes
                  // it doesn't convert them back (not sure why). So we have to account for that
                  // and at a performance cost
                  System.Data.DataRow myNewRecord = myWorkingData.NewRow();  //Create a New row from the table with the Proper Schema
                  foreach (System.Data.DataColumn myField in myWorkingData.Columns)
                  {
                      if (myField.DataType.Equals(typeof(System.Byte[])))
                      {
                          myNewRecord[myField.ColumnName] = Convert.FromBase64String(Convert.ToString(myRow[myField.ColumnName]));
                      }
                      else
                      {
                          myNewRecord[myField.ColumnName] = myRow[myField.ColumnName];
                      }
                  }
                  myWorkingData.Rows.Add(myNewRecord);
      
              }
      
              //We have to accept the changes because all rows are currently marked as "Added" (via JSON as well)
              myWorkingData.AcceptChanges();
      
      
              //Now restore their Row States 
              for (Int32 intRowIndex = 0; intRowIndex < myWorkingData.Rows.Count; intRowIndex++)
              {
                  switch (_DeconstructedDataRows[intRowIndex].RowState)
                  {
                      case System.Data.DataRowState.Added:
                          myWorkingData.Rows[intRowIndex].SetAdded();
                          break;
      
                      case System.Data.DataRowState.Deleted:
                          myWorkingData.Rows[intRowIndex].Delete();
                          break;
      
                      case System.Data.DataRowState.Modified:
                          //For Modified, we have to do some kludge stuff or else the UPDATE will not trigger
                          //We start by saving off the Values that are in the Record NOW (aka the New values)
                          object[] objNewValues = myWorkingData.Rows[intRowIndex].ItemArray;
      
                          //Now we replace those values with the ORIGINAL values we saved off before transporting
                          for (Int32 intFieldIndex = 0; intFieldIndex < this._DeconstructedDataRows[intRowIndex].RowOriginalValues.Count; intFieldIndex++)
                          {
                              if (this._DeconstructedDataRows[intRowIndex].RowOriginalValues[intFieldIndex] == null)
                              {
                                  this._DeconstructedDataRows[intRowIndex].RowOriginalValues[intFieldIndex] = DBNull.Value;
                              }
                          }
                          myWorkingData.Rows[intRowIndex].ItemArray = this._DeconstructedDataRows[intRowIndex].RowOriginalValues.ToArray();
                          myWorkingData.Rows[intRowIndex].AcceptChanges();
      
                          //and Last we replace those Original values with the New Values, which not only
                          //correctly sets the Original\Proposed values, but also changes the RowState to MODIFIED
                          myWorkingData.Rows[intRowIndex].ItemArray = objNewValues; 
                          break;
      
                      default:
                          //These would be the Unchanged
                          break;
                  }
              }
      
              //And finally, we replace the existing Table with our fixed one.
              this.Data = myWorkingData;
      
          }
      

      为了测试这一点,我在我的数据库中创建了 2 条记录

      Rec1_Delete Rec2_Modify

      然后我使用上述方法加载表,将删除记录标记为删除,修改修改记录中的字段,并添加全新记录。

      然后我将数据表发送回 API 和数据库中,删除了已删除的记录,修改了修改的记录,并添加了新记录。

      作为附注,我公开了实际的 DataTable,因为如果您碰巧从 .NET 以外的其他东西(如 HTML 文件中的 Javascript)调用它。您仍然可以直接使用 DataTable 来读取和显示数据。整个 DeConstruct 和 ReConstruct 都是针对 .NET 客户端的,同时仍然允许其他客户端进行有限访问。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-06-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多