【问题标题】:Serialize and Deserialize CSV file as Stream将 CSV 文件序列化和反序列化为流
【发布时间】:2020-09-09 22:00:00
【问题描述】:

我正在尝试通过我自己的 API 将 Stream 的数据发送到 API。第三方 API 获取一个对象,该对象有一个对象属性,其属性值为Stream。在使用我的 API 的代码中,我需要读取一个 CSV 文件,然后序列化包含 Stream 属性的 DTO 对象以将其发送到我的 API。然后,我的 API 会将 DTO 对象传递给第三方 API。

我的问题是我目前正在将 Stream 属性序列化为 base 64 字符串,但无法将其反序列化回 DTO 对象。

DTO 对象:

public class BulkLeadRequest
{
    public Format3 FileFormat { get; set; }

    public FileParameter FileParameter { get; set; }

    public string LookupField { get; set; }

    public string PartitionName { get; set; }

    public int? ListId { get; set; }

    public int? BatchId { get; set; }
}

Format3 是一个枚举:

public enum Format3
{
    [System.Runtime.Serialization.EnumMember(Value = @"csv")]
    Csv = 0,

    [System.Runtime.Serialization.EnumMember(Value = @"tsv")]
    Tsv = 1,

    [System.Runtime.Serialization.EnumMember(Value = @"ssv")]
    Ssv = 2,
}

FileParamter 具有Stream 属性的对象:

public partial class FileParameter
{
    public FileParameter(System.IO.Stream data)
        : this(data, null)
    {
    }

    public FileParameter(System.IO.Stream data, string fileName)
        : this(data, fileName, null)
    {
    }

    public FileParameter(System.IO.Stream data, string fileName, string contentType)
    {
        Data = data;
        FileName = fileName;
        ContentType = contentType;
    }

    public System.IO.Stream Data { get; private set; }

    public string FileName { get; private set; }

    public string ContentType { get; private set; }
}

为了测试序列化我有这个小单元测试。

public void TestSerializationDeserialization()
{
    BulkLeadRequest bulkLeadRequest = new BulkLeadRequest();
    bulkLeadRequest.FileFormat = Format3.Csv;
    bulkLeadRequest.FileParameter = new FileParameter(new StreamReader("C:\temp\\SmallFile.csv").BaseStream);

    string serializedObject = JsonConvert.SerializeObject(bulkLeadRequest, new StreamStringConverter());

    var obj = JsonConvert.DeserializeObject<BulkLeadRequest>(serializedObject,
        new JsonSerializerSettings {Converters = new List<JsonConverter> {new StreamStringConverter()}}); // Issue here

    Assert.Equal(bulkLeadRequest, obj);
}

StreamStringConverter:

public class StreamStringConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Stream).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string objectContents = (string)reader.Value;
        byte[] base64Decoded = Convert.FromBase64String(objectContents);

        MemoryStream memoryStream = new MemoryStream(base64Decoded);

        return memoryStream;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        FileStream valueStream = (FileStream)value;
        byte[] fileBytes = new byte[valueStream.Length];

        valueStream.Read(fileBytes, 0, (int)valueStream.Length);

        string bytesAsString = Convert.ToBase64String(fileBytes);

        writer.WriteValue(bytesAsString);
    }
}

在我的TestSerializationDeserialization 单元测试中,我收到以下错误:

Newtonsoft.Json.dll 中出现“Newtonsoft.Json.JsonSerializationException”类型的异常,但未在用户代码中处理 找不到用于 Cocc.MarketoSvcs.Business.FileParameter 类型的构造函数。一个类应该有一个默认构造函数、一个带参数的构造函数或一个标有 JsonConstructor 属性的构造函数。路径“FileParameter.Data”,第 1 行,位置 40。

如果我向FileParameter 添加一个默认构造函数,那么在反序列化时Data 属性将为空。如果我删除 JsonConvert.DeserializeObject&lt;BulkLeadRequest&gt;(..);JsonConvert.DeserializeObject(...); 的调用中的隐式转换,我将得到一个对象,但不是 BulkLeadRequest 并且仍然是 base64 字符串,而不是我期望的 Stream 对象。

序列化:

{"FileFormat":0,"FileParameter":{"Data":"RklSU1ROQU1FLE1ETElOSVQsTEFTVE5BTUUNCkNyaXN0aW5hLE0sRGlGYWJpbw0KTmVsbHksLFBhbGFjaW9zDQpNYXR0aGV3LEEsTmV2ZXJz","FileName":null,"ContentType":null},"LookupField":null,"PartitionName":null,"ListId":null,"BatchId":null}

我做错了什么?

【问题讨论】:

    标签: c# serialization json.net deserialization


    【解决方案1】:

    您收到此错误是因为序列化程序不知道如何实例化您的 FileParameter 类。如果可能,它更喜欢使用默认构造函数。它可以在某些情况下使用参数化构造函数,如果你通过用[JsonConstructor]属性标记它来提示它是哪个构造函数并且所有构​​造函数参数都匹配到 JSON 对象中的属性。但是,这不适用于您的情况,因为您的所有构造函数都期望 Stream 并且您需要对此进行特殊处理。

    要解决这个问题,您需要一种方法让序列化程序在没有流的情况下实例化您的类,然后然后使用转换器来创建流。您说您尝试添加默认构造函数,但随后流为空。它不起作用的原因是因为您所有的二传手都是私人的。使用默认构造函数,序列化程序能够实例化 FileParameter,但无法填充它。

    有几种方法可以让它发挥作用。

    解决方案 1 - 修改 FileParameter 类

    1. 像以前一样向FileParameter 类添加一个默认构造函数。如果您还用[JsonConstructor] 标记它,它可以是私有的。
    2. [JsonProperty] 属性添加到需要从 JSON 填充的所有私有集属性。这将允许序列化程序写入属性,即使它们是私有的。

    所以你的班级应该是这样的:

    public partial class FileParameter
    {
        public FileParameter(System.IO.Stream data)
            : this(data, null)
        {
        }
    
        public FileParameter(System.IO.Stream data, string fileName)
            : this(data, fileName, null)
        {
        }
    
        public FileParameter(System.IO.Stream data, string fileName, string contentType)
        {
            Data = data;
            FileName = fileName;
            ContentType = contentType;
        }
    
        [JsonConstructor]
        private FileParameter()
        {
        }
    
        [JsonProperty]
        public System.IO.Stream Data { get; private set; }
    
        [JsonProperty]
        public string FileName { get; private set; }
    
        [JsonProperty]
        public string ContentType { get; private set; }
    }
    

    这是一个工作演示:https://dotnetfiddle.net/371ggK

    解决方案 2 - 使用 ContractResolver

    如果您无法轻松修改 FileParameter 类,因为它是生成的或者您不拥有代码,您可以使用自定义 ContractResolver 以编程方式执行相同的操作。以下是您需要的代码。

    public class CustomContractResolver : DefaultContractResolver
    {
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            JsonObjectContract contract = base.CreateObjectContract(objectType);
            if (objectType == typeof(FileParameter))
            {
                // inject a "constructor" to use when creating FileParameter instances
                contract.DefaultCreator = () => new FileParameter(null);
    
                // make the private properties writable
                string[] propNames = new string[]
                {
                    nameof(FileParameter.Data),
                    nameof(FileParameter.FileName),
                    nameof(FileParameter.ContentType),
                };
                foreach (JsonProperty prop in contract.Properties.Where(p => propNames.Contains(p.UnderlyingName)))
                {
                    prop.Writable = true;
                }
            }
            return contract;
        }
    }
    

    要使用解析器,请将其与StreamStringConverter 一起添加到JsonSerializerSettings

    var obj = JsonConvert.DeserializeObject<BulkLeadRequest>(serializedObject,
        new JsonSerializerSettings 
        { 
            ContractResolver = new CustomContractResolver(),
            Converters = new List<JsonConverter> { new StreamStringConverter() } 
        }); 
    

    工作演示:https://dotnetfiddle.net/VHf359


    顺便说一句,在你的StreamStringConverterWriteJson 方法中,我注意到你正在将value 转换为FileStream,即使CanConvert 方法说它可以处理任何 em>Stream。您可以通过更改此行来修复它:

    FileStream valueStream = (FileStream)value;
    

    到这里:

    Stream valueStream = (Stream)value;
    

    【讨论】:

    • 我可以添加默认构造函数,但请注意,这个类是实际生成的partial 类。有没有办法在不接触生成代码的情况下将[JsonProperty] 添加到属性中?我可以在另一个部分类实现中添加默认构造函数。
    • 是的,您可以使用自定义ContractResolver 以编程方式进行相同的更改,而不是直接修改类。我已经更新了我的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-17
    相关资源
    最近更新 更多