【问题标题】:C# Creating a DataTable from not fully-known JSONC# 从不完全已知的 JSON 创建数据表
【发布时间】:2018-09-28 15:23:16
【问题描述】:

我有一个用 C# 编写的 Azure 函数,它通过 HTTP POST 请求接收 JSON,我确定其中的节点:ssid、dim、type、list_of_business_keys。列表中的每个项目都有一个或多个列,在我收到 JSON 之前,我不知道其名称和类型。假设对于第一个示例,它们是 column_1 和 column_2,均为 Int64 类型:

{ "ssid" : 1, 
  "dim" : 2,
  "type" : 3,
  "list_of_business_keys":
  [
      {"business_key" : {"column_1" : 100, "column_2" : 1000}},
      {"business_key" : {"column_1" : 200, "column_2" : 1000}},
      {"business_key" : {"column_1" : 300, "column_2" : 1000}},
      {"business_key" : {"column_1" : 400, "column_2" : 1000}},
      {"business_key" : {"column_1" : 500, "column_2" : 1000}}
  ]
}

我想要实现的是将此 JSON 转换为 DataTable,稍后我将使用它作为表类型参数从 Azure SQL 数据库调用存储过程。所以我希望这个 DataTable 看起来像这样:

我编写了以下代码来实现这一点:

#r "Microsoft.WindowsAzure.Storage"
#r "Newtonsoft.Json"
#r "System.Net"
#r "System.Data"

using System;
using System.Net;
using System.Data;
using System.Data.SqlClient;
using Microsoft.WindowsAzure.Storage.Table;
using Newtonsoft.Json;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    string resultAsString = await req.Content.ReadAsStringAsync();

    KeyList keyList = JsonConvert.DeserializeObject<KeyList>(resultAsString);

    List<ListOfBusinessKey> list = keyList.list_of_business_keys;

    DataTable tbl = new DataTable();

    tbl.Columns.Add(new DataColumn("ssid", typeof(Int64)));
    tbl.Columns.Add(new DataColumn("dim", typeof(Int64)));
    tbl.Columns.Add(new DataColumn("type", typeof(Int64)));
    tbl.Columns.Add(new DataColumn("column_1", typeof(Int64)));
    tbl.Columns.Add(new DataColumn("column_2", typeof(Int64)));



    foreach (var key in list) {
        tbl.Rows.Add(keyList.ssid, keyList.dim, keyList.type, key.business_key.column_1, key.business_key.column_2);  
    }

    foreach (var row in tbl.Rows){
        log.Info(row.ToString());
    }

    foreach (DataRow dataRow in tbl.Rows)
    {
        foreach (var item in dataRow.ItemArray)
        {
            log.Info(item.ToString());
        }
    }

    return req.CreateResponse(keyList);

}

public class BusinessKey
{
    public int column_1 { get; set; }
    public int column_2 { get; set; }
}

public class ListOfBusinessKey
{
    public BusinessKey business_key { get; set; }
}

public class KeyList
{
    public int ssid { get; set; }
    public int dim { get; set; }
    public int type { get; set; }
    public List<ListOfBusinessKey> list_of_business_keys { get; set; }
}

这似乎有效。

我的问题是,要反序列化收到的 JSON,我必须基于 JSON 的结构创建类。创建数据表也是如此。我知道 col 名称及其类型,所以我隐式地创建了 DataTable。但是如果我以前不知道结构会发生什么?

现在假设我收到以下 JSON:

{ "ssid" : 1, 
  "dim" : 2,
  "type" : 3,
  "list_of_business_keys":
  [
      {"business_key" : {"xxx" : "abc", "yyy" : 1000, "zzz" : 123}},
      {"business_key" : {"xxx" : "cde", "yyy" : 1000, "zzz" : 456}},
      {"business_key" : {"xxx" : "efg", "yyy" : 1000, "zzz" : 789}},
      {"business_key" : {"xxx" : "hij", "yyy" : 1000, "zzz" : 12 }},
      {"business_key" : {"xxx" : "klm", "yyy" : 1000, "zzz" : 345}}
  ]
}

我想收到以下 DT:

这是否可以更改代码,以“动态”将 JSON 转换为所需的 DataTable 格式?如果是,我应该怎么做才能做到这一点?

提前致谢!

【问题讨论】:

  • 有多少 JSON 模式是固定的和可变的?属性"ssid""dim""type" 是否已修复? "list_of_business_keys""business_key" 属性是否固定?
  • "ssid", "dim", "type", "list_of_business_keys""business_key" 已修复。只有business key(本例中为"xxx", "yyy", "zzz")内的节点是可更改的。当然,在一个 JSON 中,所有这些业务键列都将具有相同的名称(没有情况,我们在一行中有 xxx,而在第二行中有 aaa
  • 我也想知道,如果使用像 {"ssid" : 1, "dim" : 2, "type" : 3, {"business_key" : {"xxx" : "abc", "yyy" : 1000, "zzz" : 123}}},{"ssid" : 1, "dim" : 2, "type" : 3, {"business_key" : {"xxx" : "abc", "yyy" : 1000, "zzz" : 123}}} 这样的 JSON 会更简单

标签: c# asp.net json parsing azure-functions


【解决方案1】:

您可以将business_key 定义为Dictionary&lt;string, object&gt;,然后根据JSON 文件中实际遇到的属性名称和数据类型动态填充DataTable

定义以下类型和扩展方法:

public class ListOfBusinessKey
{
    public Dictionary<string, object> business_key { get; set; }
}

public class KeyList
{
    public int ssid { get; set; }
    public int dim { get; set; }
    public int type { get; set; }
    public List<ListOfBusinessKey> list_of_business_keys { get; set; }

    public DataTable ToDataTable()
    {
        var tbl = new DataTable();
        tbl.Columns.Add(new DataColumn("ssid", typeof(Int64)));
        tbl.Columns.Add(new DataColumn("dim", typeof(Int64)));
        tbl.Columns.Add(new DataColumn("type", typeof(Int64)));

        var columnQuery = EnumerableExtensions.Merge(
            list_of_business_keys
            .SelectMany(k => k.business_key)
            .Select(p => new KeyValuePair<string, Type>(p.Key, p.Value == null ? typeof(object) : p.Value.GetType())),
            p => p.Key, (p1, p2) => new KeyValuePair<string, Type>(p1.Key, MergeTypes(p1.Value, p2.Value)));
        foreach (var c in columnQuery)
            tbl.Columns.Add(c.Key, c.Value);

        foreach (var d in list_of_business_keys.Select(k => k.business_key))
        {
            var row = tbl.NewRow();
            row["ssid"] = ssid;
            row["dim"] = dim;
            row["type"] = type;
            foreach (var p in d.Where(p => p.Value != null))
            {
                row[p.Key] = Convert.ChangeType(p.Value, tbl.Columns[p.Key].DataType, CultureInfo.InvariantCulture);
            }
            tbl.Rows.Add(row);
        }
        return tbl;
    }

    static Type MergeTypes(Type type1, Type type2)
    {
        // Enhance as needed
        if (type1 == type2)
            return type1;
        if (type2 == typeof(object))
            return type1;
        if (type1 == typeof(object))
            return type2;
        if (type1.IsAssignableFrom(type2))
            return type1;
        if (type2.IsAssignableFrom(type1))
            return type2;
        if (typeof(IConvertible).IsAssignableFrom(type1) && typeof(IConvertible).IsAssignableFrom(type2))
        {
            if (type1 == typeof(string))
                return type1;
            if (type2 == typeof(string))
                return type2;
            if ((type1 == typeof(long) || type1 == typeof(int)) && (type2 == typeof(decimal) || type2 == typeof(double)))
                return type2;
            if ((type2 == typeof(long) || type2 == typeof(int)) && (type1 == typeof(decimal) || type1 == typeof(double)))
                return type1;
        }
        throw new ArgumentException(string.Format("Cannot merge types {0} and {1}", type1, type2));
    }
}

public static class EnumerableExtensions
{
    public static IEnumerable<TSource> Merge<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> mergeSelector)
    {
        if (source == null || keySelector == null || mergeSelector == null)
            throw new ArgumentNullException();
        return MergeIterator(source, keySelector, mergeSelector);
    }

    static IEnumerable<TSource> MergeIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> mergeSelector)
    {
        var dictionary = new Dictionary<TKey, TSource>();
        foreach (TSource element in source)
        {
            var key = keySelector(element);
            TSource oldElement;
            if (!dictionary.TryGetValue(key, out oldElement))
            {
                dictionary[key] = element;
            }
            else
            {
                dictionary[key] = mergeSelector(element, oldElement);
            }
        }
        return dictionary.Values;
    }
}

然后反序列化并转换为DataTable,如下:

var keyList = JsonConvert.DeserializeObject<KeyList>(json);
var tbl = keyList.ToDataTable();

请注意,处理"business_key" 属性之一首先遇到空值然后遇到非空值,或者首先遇到整数值然后遇到十进制或双精度值。在这种情况下,需要优先使用更通用的类型而不是最初遇到的类型。

样本工作.Net fiddle

更新

EnumerableExtensions.Merge()MergeTypes() 的目的是通过查看将添加到列中的所有单元格并选择“最合适”的类型来推断正确的 Type 用于DataColumn.DataType合并每个单元格的观察类型。与仅查看第一行中的单元格相比,这可以处理以下情况:

  1. 第一行包含一个null 单元格。在这种情况下,必须检查后续行。
  2. 第一行包含一个像123 这样的整数值的单元格,但随后的一行有一个像12.123 这样的浮点值的单元格。在这种情况下,推断的类型需要从long 切换到doubledecimal
  3. 第一行包含一个单元格,其数值类似于1000,但后续行的单元格具有一个字符串值,例如"Unknown""$122.22"(或其他)。在这种情况下,推断的类型需要切换到string
  4. 将添加到同一列的两个单元格具有完全不兼容的类型,例如longDateTime。在这种情况下会引发异常。

Newtonsoft 自己的DataTableConverter 仅从第一行推断DataColumn.DataType,这会周期性地导致类似于 DateTime column type becomes String type after deserializing DataTabledeserialize a datatable with a missing first column 的问题。此答案中的代码利用了整个 JSON 已预加载到 JToken 层次结构中这一事实来避免这些问题。

example fiddle 中的第三个 JSON 字符串示例包括案例 #1-#3 的示例。

如果您确定以上都不会发生,您可以简化MergeTypes(),以便在类型不相同时简单地抛出异常。

【讨论】:

  • 嗨@dbc,这是一段完美的代码,确实可以实现我试图实现的目标。它在 VS 中运行良好,但是当我尝试使用 .csx 脚本中的代码时出现问题(这是我在 C# 中编写 Azure Function 时使用的)。对于每个扩展功能,我都会收到类似的错误消息:[Error] run.csx(71,33): error CS1109: Extension methods must be defined in a top level static class; KeyListExtensions is a nested class。有没有办法以某种方式解决它?当然,所有这些类都不是嵌套的。
  • Environment version: 4.0.30319.42000 Json.NET version: Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed 如果需要的话
  • @Jangcy - 似乎这是 csx 脚本的限制,请参阅 How to define an extension method in a scriptcs csx script。解决方案是将扩展方法变成对象方法或普通的旧静态方法。答案已更新。
  • 我已经设法通过在另一个 .csx 文件中添加扩展方法并将其加载到主文件中来解决该限制。经过一些修改后,您的方法就像一个魅力!非常感谢!
  • 嗨@dbc。我知道现在已经有一段时间了,但是您能否解释一下您的方法:merge、mergeIterator、mergeSelector、mergeTypes 是做什么的?我只是想确保我完全理解这段代码中发生的事情。提前致谢。
【解决方案2】:

它闻起来像是 NoSql 数据库的解决方案。例如Azure 表存储 不同的对象类型可以有不同的列名。

Azure table storage store multiple types

【讨论】:

    【解决方案3】:

    首先创建将jsonstring转换为数据表的函数:

       public DataTable JsonStringToDataTable(string jsonString)
       {
          DataTable dt = new DataTable();
          string[] jsonStringArray = Regex.Split(jsonString.Replace("[", "").Replace("]", ""), "},{");
          List<string> ColumnsName = new List<string>();
          foreach (string jSA in jsonStringArray)
          {
             string[] jsonStringData = Regex.Split(jSA.Replace("{", "").Replace("}", ""), ",");
             foreach (string ColumnsNameData in jsonStringData)
             {
                try
                {
                   int idx = ColumnsNameData.IndexOf(":");
                   string ColumnsNameString = ColumnsNameData.Substring(0, idx - 1).Replace("\"", "");
                   if (!ColumnsName.Contains(ColumnsNameString))
                   {
                      ColumnsName.Add(ColumnsNameString);
                   }
                }
                catch (Exception ex)
                {
                   throw new Exception(string.Format("Error Parsing Column Name : {0}", ColumnsNameData));
                }
             }
             break;
          }
          foreach (string AddColumnName in ColumnsName)
          {
             dt.Columns.Add(AddColumnName);
          }
          foreach (string jSA in jsonStringArray)
          {
             string[] RowData = Regex.Split(jSA.Replace("{", "").Replace("}", ""), ",");
             DataRow nr = dt.NewRow();
             foreach (string rowData in RowData)
             {
                try
                {
                   int idx = rowData.IndexOf(":");
                   string RowColumns = rowData.Substring(0, idx - 1).Replace("\"", "");
                   string RowDataString = rowData.Substring(idx + 1).Replace("\"", "");
                   nr[RowColumns] = RowDataString;
                }
                catch (Exception ex)
                {
                   continue;
                }
             }
             dt.Rows.Add(nr);
          }
          return dt;
       }
    

    然后调用这个函数:

     string FileName = "JSONString.txt";
     var stream = File.OpenText(Server.MapPath(FileName));
     string JsonString = stream.ReadToEnd();
     DataTable dt = JsonStringToDataTable(JsonString); 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-07
      相关资源
      最近更新 更多