Сериализованный объект ASP.NET Core с FileInfo возвращает неполный JSON - PullRequest
3 голосов
/ 19 июня 2019

У меня есть проект ASP.NET Core 2.2 с контроллером, метод GET которого возвращает объект, содержащий свойство System.IO.FileInfo.Когда я вызываю API (например, в веб-браузере), он возвращает неполную строку JSON.

Вот класс, экземпляр которого сериализуется:

public class Thing
{
    public string Name { get; set; }
    public FileInfo File { get; set; }
}

Вот контроллер, программа,и классы запуска:

[Route("Test/Home")]
[ApiController]
public class HomeController : Controller
{
    [HttpGet]
    public async Task<ActionResult<Thing>> GetThing()
    {
        return new Thing()
        {
            Name = "First thing",
            File = new FileInfo("c:\file.txt")
        };
    }
}

public class Program
{
    public static async Task Main(string[] args) 
        => await CreateWebHostBuilder(args).Build().RunAsync();

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        services.AddSingleton<Thing>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseHttpsRedirection();
        app.UseMvc();
    }
}

Вот URL:

https://localhost:44381/Test/Home

И результат, который я получаю:

{"$id":"1","name":"First thing","file":

Так почему строка JSON неполнаяобломается на объекте FileInfo?FileInfo является сериализуемым .

Вот полный проект, если вы хотите попробовать его самостоятельно:

https://github.com/roryap/FileInfoAspNetCoreIssue

Все ссылки, которые я нашел, охватывают такие вещи, какниже приведены разговоры об EF Core и циклических ссылках, что здесь явно не так.

https://stackoverflow.com/a/56365960/2704659

https://stackoverflow.com/a/54633487/2704659

https://stackoverflow.com/a/49224944/2704659

1 Ответ

3 голосов
/ 20 июня 2019

Основная проблема здесь заключается в том, что документация для FileInfo в netcore-2.2 просто неверна - FileInfo на самом деле не помечена [Serializable] в ядре .Net.Без [Serializable] Json.NET будет пытаться сериализовать открытые свойства FileInfo, а не его данные ISerializable, что в конечном итоге приведет к исключению переполнения стека хотя бы для одного из свойств, FileInfo.Directory.Root.Root....Возвращенный JSON затем усекается в момент, когда генерируется исключение, так как сервер уже начал писать ответ в этот момент.

(На самом деле кажется, что FileInfo занесен в черный список на ядре .Net, чтобы избежатьпереполнение стека, см. Проблема # 1541: исключение StackOverflowException при сериализации объекта DirectoryInfo в ядре dotnet 2 . Вместо этого выдается пользовательское исключение.)

Чтобы подтвердить ошибку документации, ссылка Источник для .Net core (зеркальный здесь ) показывает, что FileInfo должен быть объявлен следующим образом (хотя он объявлен как partial, он, кажется, имеет только один файл):

// Class for creating FileStream objects, and some basic file management
// routines such as Delete, etc.
public sealed partial class FileInfo : FileSystemInfo
{

В то время как справочный источник для полной платформы показывает следующее:

// Class for creating FileStream objects, and some basic file management
// routines such as Delete, etc.
[Serializable]
[ComVisible(true)]
public sealed class FileInfo: FileSystemInfo
{

Без атрибута [Serializable] Json.NET будет игнорировать интерфейс ISerializable в базовом классе.как описано в примечаниях к выпуску Json.NET 11 :

  • Изменение - типы, которые реализуют ISerializable, но не имеют [SerializableAttribute], являютсяне сериализовано с использованием ISerializable

Итак, что можно сделать?Одной из возможностей может быть создание пользовательского преобразователя контракта , который вынуждает сериализовать FileInfo с использованием интерфейса ISerializable:

public class FileInfoContractResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType == typeof(FileInfo))
        {
            return CreateISerializableContract(objectType);
        }

        var contract = base.CreateContract(objectType);
        return contract;
    }
}

Настройте преобразователь контракта, как показано, например, в Настройка JsonConvert.DefaultSettings asp net core 2.0 не работает должным образом .

Другой возможностью будет создание custom JsonConverter для FileInfo, который сериализует и десериализует те же свойства, что и полный каркас:

public class ISerializableJsonConverter<T> : JsonConverter where T : ISerializable
{
    // Simplified from 
    //  - JsonSerializerInternalReader.CreateISerializable()
    //    https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1708
    //  - JsonSerializerInternalWriter.SerializeISerializable()
    //    https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs#L837
    // By James Newton-King http://james.newtonking.com/
    // Not implemented: 
    // PreserveReferencesHandling, TypeNameHandling, ReferenceLoopHandling, NullValueHandling   

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));

        SerializationInfo serializationInfo = new SerializationInfo(objectType, new JsonFormatterConverter(serializer));

        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
        {
            switch (reader.TokenType)
            {
                case JsonToken.PropertyName:
                    serializationInfo.AddValue((string)reader.Value, JToken.ReadFrom(reader.ReadToContentAndAssert())); 
                    break;

                default:
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }

        return Activator.CreateInstance(objectType, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { serializationInfo, serializer.Context }, serializer.Culture);
    }

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

        SerializationInfo serializationInfo = new SerializationInfo(value.GetType(), new FormatterConverter());
        serializable.GetObjectData(serializationInfo, serializer.Context);

        writer.WriteStartObject();

        foreach (SerializationEntry serializationEntry in serializationInfo)
        {
            writer.WritePropertyName(serializationEntry.Name);
            serializer.Serialize(writer, serializationEntry.Value);
        }

        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static JsonReader ReadToContentAndAssert(this JsonReader reader)
    {
        return reader.ReadAndAssert().MoveToContentAndAssert();
    }

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

internal class JsonFormatterConverter : IFormatterConverter
{
    //Adapted and simplified from 
    // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/FormatterConverter.cs
    // By James Newton-King http://james.newtonking.com/
    JsonSerializer serializer;

    public JsonFormatterConverter(JsonSerializer serializer)
    {
        this.serializer = serializer;
    }

    private T GetTokenValue<T>(object value)
    {
        JValue v = (JValue)value;
        return (T)System.Convert.ChangeType(v.Value, typeof(T), CultureInfo.InvariantCulture);
    }

    public object Convert(object value, Type type)
    {
        if (!(value is JToken))
        {
            throw new ArgumentException("Value is not a JToken.", "value");
        }

        return ((JToken)value).ToObject(type, serializer);
    }

    public object Convert(object value, TypeCode typeCode)
    {
        if (value is JValue)
        {
            value = ((JValue)value).Value;
        }

        return System.Convert.ChangeType(value, typeCode, CultureInfo.InvariantCulture);
    }

    public bool ToBoolean(object value)
    {
        return GetTokenValue<bool>(value);
    }

    public byte ToByte(object value)
    {
        return GetTokenValue<byte>(value);
    }

    public char ToChar(object value)
    {
        return GetTokenValue<char>(value);
    }

    public DateTime ToDateTime(object value)
    {
        return GetTokenValue<DateTime>(value);
    }

    public decimal ToDecimal(object value)
    {
        return GetTokenValue<decimal>(value);
    }

    public double ToDouble(object value)
    {
        return GetTokenValue<double>(value);
    }

    public short ToInt16(object value)
    {
        return GetTokenValue<short>(value);
    }

    public int ToInt32(object value)
    {
        return GetTokenValue<int>(value);
    }

    public long ToInt64(object value)
    {
        return GetTokenValue<long>(value);
    }

    public sbyte ToSByte(object value)
    {
        return GetTokenValue<sbyte>(value);
    }

    public float ToSingle(object value)
    {
        return GetTokenValue<float>(value);
    }

    public string ToString(object value)
    {
        return GetTokenValue<string>(value);
    }

    public ushort ToUInt16(object value)
    {
        return GetTokenValue<ushort>(value);
    }

    public uint ToUInt32(object value)
    {
        return GetTokenValue<uint>(value);
    }

    public ulong ToUInt64(object value)
    {
        return GetTokenValue<ulong>(value);
    }
}

, а затем добавляет new ISerializableJsonConverter<FileInfo>() к JsonSerializerSettings.Converters.

Примечания:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...