【问题标题】:Error converting value from string to stream将值从字符串转换为流时出错
【发布时间】:2017-03-16 23:54:43
【问题描述】:

我想序列化一个自定义对象:

public class MyCustomObject
{
    public string Name { get; set; }
    public DateTime Date { get; set; }
    public List<HttpPostedFileBase> Files { get; set; }

    public MyCustomObject()
    {
        Files = new List<HttpPostedFileBase>();
    }
}

在 json 中。为此,我使用了自定义转换器:

public class HttpPostedFileConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var stream = (Stream)value;
        using (var sr = new BinaryReader(stream))
        {
            var buffer = sr.ReadBytes((int)stream.Length);
            writer.WriteValue(Convert.ToBase64String(buffer));
        }
    }

我使用 JsonSerializerSettings 来序列化到 json.net 知道哪种类型实现(用于 HttpPostedFileBase)。

        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new HttpPostedFileConverter());
        settings.TypeNameHandling = TypeNameHandling.Objects;

对象已正​​确序列化,但序列化时出现此错误:

JsonSerializationException Error converting value "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDA

这是我的序列化对象的值:

{
  "$type": "ConsoleApplication1.MyCustomObject, ConsoleApplication1",
  "Name": "Test2",
  "Date": "2016-11-03T12:35:14.6020154+01:00",
  "Files": [
    {
      "$type": "System.Web.HttpPostedFileWrapper, System.Web",
      "ContentLength": 1024,
      "FileName": "Pannigale.jpg",
      "ContentType": "image/jpg",
      "InputStream": "/9j/4AAQ...KKAP//Z"
    }
  ]
}

反序列化出了什么问题?

编辑 我已经测试了一个类来测试......现在它可以工作了:

public class TestHttpFile : HttpPostedFileBase
{
    string fullFileName = @"C:\Pictures\SBK-1299-Panigale-S_2015_Studio_R_B01_1920x1080.mediagallery_output_image_[1920x1080].jpg";
    public override int ContentLength
    {
        get
        {
            return 1024;
        }
    }

    public override string FileName
    {
        get
        {
            return "Pannigale.jpg";
        }
    }

    public override string ContentType
    {
        get
        {
            return "image/jpg";
        }
    }

    public override Stream InputStream
    {
        get
        {
            return File.OpenRead(fullFileName);
        }
    }
}

在序列化中我注意到了这个区别:

 "$type": "ConsoleApplication1.TestHttpFile, ConsoleApplication1",

而不是

 "$type": "System.Web.HttpPostedFileWrapper, System.Web",

但最后我不想创建包装器或其他任何东西......而且我不明白为什么它适用于这种类型而不适用于 HttpPostedFileWrapper

【问题讨论】:

  • 好吧,您只在转换器中提供了 一个 方向。听起来您还需要覆盖 ReadJson...
  • 这是我的想法但是转换器在反序列化时不使用转换器的ReadJson或CanRead方法
  • 您是否也覆盖了CanConvert? (基本上,听起来你真正想要的是StreamConverter,而不是HttpPostedFileConverter。)
  • 我试试看。谢谢!

标签: c# json serialization json.net deserialization


【解决方案1】:

为什么反序列化失败

简而言之:您尝试反序列化的类型,即HttpPostedFileWrapper 及其底层HttpPostedFile,不可公开构造。

大概你的HttpPostedFileConverter实际上是一个流转换器,它的工作原理是这样的:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var bytes = serializer.Deserialize<byte[]>(reader);
        return new MemoryStream(bytes);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var stream = (Stream)value;
        var bytes = stream.ReadAllBytesAndReposition();
        serializer.Serialize(writer, bytes);
    }
}

public static class StreamExtensions
{
    public static byte[] ReadAllBytesAndReposition(this Stream stream)
    {
        const int bufferSize = 4096;
        using (var ms = new MemoryStream())
        {
            byte[] buffer = new byte[bufferSize];
            int count;
            var position = stream.CanSeek ? stream.Position : (long?)null;
            while ((count = stream.Read(buffer, 0, buffer.Length)) != 0)
                ms.Write(buffer, 0, count);
            if (position != null)
            {
                // Restore position
                stream.Position = position.Value;
            }
            return ms.ToArray();
        }
    }
}

在这种情况下,我可以为您的 MyCustomObject 类生成类似于您的 JSON,假设我还在序列化程序设置中设置了 TypeNameHandling = TypeNameHandling.Auto。但是,反序列化会失败,因为具体类型System.Web.HttpPostedFileWrapper 只有一个构造函数,它采用HttpPostedFile。来自reference source

public class HttpPostedFileWrapper : HttpPostedFileBase {

    private HttpPostedFile _file;

    public HttpPostedFileWrapper(HttpPostedFile httpPostedFile) {
        if (httpPostedFile == null) {
            throw new ArgumentNullException("httpPostedFile");
        }
        _file = httpPostedFile;
    }

由于构造函数是公共的,Json.NET 将调用它,但由于 JSON 中没有名为 httpPostedFile 的属性,它将为该值传递 null,从而导致抛出 ArgumentNullException

这给我们带来了下一个问题:HttpPostedFile 没有公共构造函数。相反,要直接创建一个,您必须通过反射调用几种不同的 Microsoft 内部方法,如How to instantiate a HttpPostedFile 所示。因此,即使您为HttpPostedFileWrapper 创建了custom JsonConverter,在内部构造必要的HttpPostedFile 也是非常棘手的。 Json.NET 当然不能自动完成。

那么,你有什么选择?

选项 1:创建您自己的 HttpPostedFilesBase 子类型

您可以创建自己的HttpPostedFilesBase 子类型,它可以成功地进行序列化和反序列化,然后在序列化期间使用适当的JsonConverterHttpPostedFilesBase 的所有实例映射到此类型。以下类型MemoryHttpPostedFile 执行此操作:

public sealed class MemoryHttpPostedFile : HttpPostedFileBase
{
    readonly string contentType;
    readonly string fileName;
    readonly MemoryStream inputStream;

    public MemoryHttpPostedFile(string contentType, string fileName, [JsonConverter(typeof(StreamConverter))] MemoryStream inputStream)
    {
        if (inputStream == null)
            throw new ArgumentNullException("inputStream");
        this.contentType = contentType;
        this.fileName = fileName;
        this.inputStream = inputStream;
    }

    public override int ContentLength { get { return (int)inputStream.Length; } }

    public override string ContentType { get { return contentType; } }

    public override string FileName { get { return fileName; } }

    [JsonConverter(typeof(StreamConverter))]
    public override Stream InputStream { get { return inputStream; } }

    //TODO: implement SaveAs()
    public override void SaveAs(string filename)
    {
        // Implement based on HttpPostedFile.SaveAs()
        // https://referencesource.microsoft.com/#System.Web/HttpPostedFile.cs,678e7f8bc95c149f
        throw new NotImplementedException();
    }
}

public class HttpPostedFileBaseConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(HttpPostedFileBase).IsAssignableFrom(objectType)
            && !typeof(MemoryHttpPostedFile).IsAssignableFrom(objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var postedFile = (HttpPostedFileBase)value;

        // Save position
        var wrapper = new MemoryHttpPostedFile(postedFile.ContentType, postedFile.FileName, new MemoryStream(postedFile.InputStream.ReadAllBytesAndReposition()));

        serializer.Serialize(writer, wrapper);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var wrapper = serializer.Deserialize<MemoryHttpPostedFile>(reader);
        return wrapper;
    }
}

由于HttpPostedFilesBase 的所有子类型都映射到MemoryHttpPostedFile,因此不再需要设置TypeNameHandling.Auto。请注意,我在此答案的前面使用了 StreamConverterStreamExtensions

将转换器添加到设置中,如下所示:

var settings = new JsonSerializerSettings
{
    Converters = new JsonConverter[] { new HttpPostedFileBaseConverter() },
    Formatting = Formatting.Indented,
};

选项 2:实际序列化和反序列化 HttpPostedFileWrapper

为此,我们需要使用从this answerHow to instantiate a HttpPostedFile by paracycle 的相当复杂和“hacky”的代码,如下所示:

public class HttpPostedFileBaseConverter : JsonConverter
{
    class HttpPostedFileSurrogate
    {
        public string ContentType { get; set; }

        public string FileName { get; set; }

        public byte[] InputStream { get; set; }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(HttpPostedFileBase).IsAssignableFrom(objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var wrapper = (HttpPostedFileWrapper)value;

        // Save position
        var surrogate = new HttpPostedFileSurrogate
        {
            ContentType = wrapper.ContentType,
            FileName = wrapper.FileName,
            InputStream = wrapper.InputStream.ReadAllBytesAndReposition(),
        };

        serializer.Serialize(writer, surrogate);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var surrogate = serializer.Deserialize<HttpPostedFileSurrogate>(reader);
        var file = HttpPostedFileExtensions.ConstructHttpPostedFile(surrogate.InputStream, surrogate.FileName, surrogate.ContentType);
        return new HttpPostedFileWrapper(file);
    }
}

public static class HttpPostedFileExtensions
{
    public static HttpPostedFile ConstructHttpPostedFile(byte[] data, string filename, string contentType)
    {
        // Adapted from https://stackoverflow.com/questions/5514715/how-to-instantiate-a-httppostedfile/5515134#5515134
        // Get the System.Web assembly reference (they seem to be in different assemblies in different versions of .Net
        var assemblies = new[] { typeof(HttpPostedFile).Assembly, typeof(HttpPostedFileBase).Assembly };

        // Get the types of the two internal types we need
        Type typeHttpRawUploadedContent = assemblies.Select(a => a.GetType("System.Web.HttpRawUploadedContent")).Where(t => t != null).First();
        Type typeHttpInputStream = assemblies.Select(a => a.GetType("System.Web.HttpInputStream")).Where(t => t != null).First();

        // Prepare the signatures of the constructors we want.
        Type[] uploadedParams = { typeof(int), typeof(int) };
        Type[] streamParams = { typeHttpRawUploadedContent, typeof(int), typeof(int) };
        Type[] parameters = { typeof(string), typeof(string), typeHttpInputStream };

        // Create an HttpRawUploadedContent instance
        object uploadedContent = typeHttpRawUploadedContent
          .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, uploadedParams, null)
          .Invoke(new object[] { data.Length, data.Length });

        // Call the AddBytes method
        typeHttpRawUploadedContent
          .GetMethod("AddBytes", BindingFlags.NonPublic | BindingFlags.Instance)
          .Invoke(uploadedContent, new object[] { data, 0, data.Length });

        // This is necessary if you will be using the returned content (ie to Save)
        typeHttpRawUploadedContent
          .GetMethod("DoneAddingBytes", BindingFlags.NonPublic | BindingFlags.Instance)
          .Invoke(uploadedContent, null);

        // Create an HttpInputStream instance
        object stream = (Stream)typeHttpInputStream
          .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, streamParams, null)
          .Invoke(new object[] { uploadedContent, 0, data.Length });

        // Create an HttpPostedFile instance
        HttpPostedFile postedFile = (HttpPostedFile)typeof(HttpPostedFile)
          .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, parameters, null)
          .Invoke(new object[] { filename, contentType, stream });

        return postedFile;
    }
}

注意我在这个答案的前面使用了StreamExtensions。这个版本的HttpPostedFileBaseConverter 也会像往常一样添加到JsonSerializerSettings.Converters

(老实说,我不推荐这种解决方案,因为它过多地依赖于 Microsoft 实现 HttpPostedFile 的未记录内部结构,在以后的版本中很容易改变。)

【讨论】:

    【解决方案2】:

    实际上有两个不同的问题。

    流转换器

    我想在 json 中序列化一个自定义对象...为此,我使用了自定义转换器...

    尽管您将其称为HttpPostedFileConverter,但发布的代码表明它所做的只是将Stream 转换为Base64 编码string。所以让我们用不同的方式调用它并实现所有方法:

    public class StreamConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(Stream).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return new MemoryStream(Convert.FromBase64String((string)reader.Value));
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var stream = (Stream)value;
            using (var sr = new BinaryReader(stream))
            {
                var buffer = sr.ReadBytes((int)stream.Length);
                writer.WriteValue(Convert.ToBase64String(buffer));
            }
        }
    }
    

    它将用于InputStream成员序列化/反序列化,所以在这两个地方你都会用到:

    settings.Converters.Add(new StreamConverter());
    

    对于序列化,这就是您所需要的。现在是棘手的部分。

    反序列化 HttpPostedFileBase 实例

    但最后我不想创建包装器或其他任何东西......而且我不明白为什么它适用于这种类型而不适用于 HttpPostedFileWrapper

    问题是用于序列化的HttpPostedFileWrapper 没有无参数构造函数。事实上,唯一可用的构造函数具有以下签名:

    public HttpPostedFileWrapper(
        HttpPostedFile httpPostedFile
    )
    

    如您所见,它需要HttpPostedFile。但是HttpPostedFile 根本没有公共构造。

    因此,无论您是否愿意,您都别无选择,只能创建一个自定义类用于反序列化 HttpPostedFileBase 实例。

    让我们这样做:

    public class CustomHttpPostedFile : HttpPostedFileBase
    {
        [JsonProperty("ContentLength")]
        private int contentLength;
        [JsonProperty("FileName")]
        private string fileName;
        [JsonProperty("ContentType")]
        private string contentType;
        [JsonProperty("InputStream")]
        private Stream inputStream;
        [JsonIgnore]
        public override int ContentLength { get { return contentLength; } }
        [JsonIgnore]
        public override string FileName { get { return fileName; } }
        [JsonIgnore]
        public override string ContentType { get { return contentType; } }
        [JsonIgnore]
        public override Stream InputStream { get { return inputStream; } }
        public override void SaveAs(string filename)
        {
            using (var output = File.Create(fileName))
                InputStream.CopyTo(output);
        }
    }
    

    但这还不够。为了让反序列化器使用这个类而不是HttpPostedFileWrapper,你需要一个CustomCreationConverter,就像Json.NET中的Deserialize with CustomCreationConverter示例:

    public class HttpPostedFileBaseConverter : CustomCreationConverter<HttpPostedFileBase>
    {
        public override HttpPostedFileBase Create(Type objectType)
        {
            return new CustomHttpPostedFile();
        }
    }
    

    并且仅在反序列化时使用:

    settings.Converters.Add(new StreamConverter());
    settings.Converters.Add(new HttpPostedFileBaseConverter());
    

    一切都会好起来的。

    回顾一下,您遇到的主要问题是由于在您的对象内部使用了抽象类及其具体实现,您无法控制并且不提供反序列化的方法(因为不是为使用而设计的完全用于序列化)。所以好处是解决方案完全存在,但需要编写一些自定义代码。

    【讨论】:

      【解决方案3】:

      HttpPostedFileBase 是一个抽象类,HttpPostedFile 是它的具体实现。当您在 ASP.Net 中从用户那里获取文件上传时,其类型实际上是 HttpPostedFile。所以你要找的包装器已经存在了,你不必再写了。

      var file = (HttpPostedFile) Request.Files[0];
      

      似乎您需要存储的不是严格意义上的HttpPostedFileHttpPostedFileBase,而是具有大小、mime 类型、文件名和流作为字节数组等属性的任意类。如果您要将对象存储在数据库等中,这更有意义。为什么不创建一个类并从您获得的HttpPostedFile 复制属性?

      编辑

      似乎 json 无法反序列化为 HttpPostedFile,因为它没有构造函数。您的选择是编写一个继承自 HttpPostedFileBase 的类或编写一个新类。

      这是我用来测试的JsonConverter:

      public class StreamConverter : JsonConverter
      {
          public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
          {
              var stream = (Stream)value;
              using (var sr = new BinaryReader(stream))
              {
                  var buffer = sr.ReadBytes((int)stream.Length);
                  writer.WriteValue(Convert.ToBase64String(buffer));
              }
          }
      
          public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
          {
              var str = reader.ReadAsString();
              var stream = new MemoryStream();
              using (var sr = new BinaryWriter(stream))
              {
                  sr.Write(Convert.FromBase64String(str));
              }
              return stream;
          }
      
          public override bool CanConvert(Type objectType)
          {
              return typeof(Stream).IsAssignableFrom(objectType);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-05-23
        • 2014-01-23
        • 2012-07-29
        • 2018-01-03
        • 2014-06-25
        • 2013-06-29
        相关资源
        最近更新 更多