json Newtonsoft如何在切换到.Net Core后再次将null反序列化为默认值

4zcjmb1e  于 2022-11-26  发布在  .NET
关注(0)|答案(2)|浏览(133)

我们将Asp.NET应用程序从.Net Framework 4.7.2更新为.Net 5。现在,我们在控制器方法中遇到了反序列化JSON的问题。在旧版本中,我们使用Newtonsoft.JSON。以前,如果我们在JSON中为不可空类型(如int)的属性获取空值,反序列化器将采用默认值。或忽略了null和错误,并且没有覆盖对象创建时属性的默认值。但是现在,在出现错误后,整个对象都被设置为null。
第一个

预期与之前类似将是Cwe = false且所有HoursXXInHours = 0的OrderEffortDto示例
我们得到的结果是OrderEffortDto = null

我们已经尝试在新版本中使用Newtonsoft,但结果相同。我们还配置了SerializerSettings.NullValueHandling = NullValueHandling.Ignore。这对该问题有效,但对于另一个方向,对于DTO到JSON的序列化,空值也会被忽略,其中需要空值。
有没有办法恢复原来的行为?是的,在前端修复这个问题,将正确的值放入JSON中是没有问题的,但是我们的应用程序很大,要确定所有需要纠正的地方,这很容易出错。

Update 1适用于可能遇到相同问题的用户

我创建了两个简单的测试项目,一个是使用.Net Framework 4.7.2的ASP.NET WebApi,另一个是使用.Net 5的ASP.NET WebApi,使用上面的JSON和DTO示例。我从Newtonsoft获得了两个类似的跟踪错误,并且已经描述了控制器中DTO的结果。另外,.Net 5中的System.Text.Json为整个DTO提供了一个空值。

  • 适用于带有.Net Framework 4.7.2的API *
2022-03-24T10:50:05.368 Info Started deserializing WebApplication1NetFramework.Data.OrderEffortDto. Path 'effortType', line 2, position 16.
2022-03-24T10:50:05.388 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Boolean'. Path 'cwe', line 3, position 14.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours25InHours', line 7, position 25.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours50InHours', line 8, position 25.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours100InHours', line 9, position 26.
2022-03-24T10:50:05.404 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours150InHours', line 10, position 26.
2022-03-24T10:50:05.404 Verbose Could not find member 'orderNumber' on WebApplication1NetFramework.Data.OrderEffortDto. Path 'orderNumber', line 11, position 17.
2022-03-24T10:50:05.405 Verbose Could not find member 'withCosts' on WebApplication1NetFramework.Data.OrderEffortDto. Path 'withCosts', line 12, position 15.
2022-03-24T10:50:05.407 Info Finished deserializing WebApplication1NetFramework.Data.OrderEffortDto. Path '', line 16, position 1.
2022-03-24T10:50:05.407 Verbose Deserialized JSON: 
{
  "effortType": "1",
  "cwe": null,
  "distanceInKilometers": null,
  "effortDate": "2022-03-22T14:45:00+01:00",
  "effortInHours": 1.0,
  "hours25InHours": null,
  "hours50InHours": null,
  "hours100InHours": null,
  "hours150InHours": null,
  "orderNumber": "006001780872",
  "withCosts": false,
  "isNew": true,
  "isEdited": false,
  "isDeleted": false
}

  • 适用于.Net 5的API *
2022-03-24T10:48:19.162 Info Started deserializing WebApplication1NetCore.Data.OrderEffortDto. Path 'effortType', line 2, position 16.
2022-03-24T10:48:19.180 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Boolean'. Path 'cwe', line 3, position 14.
2022-03-24T10:48:19.196 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours25InHours', line 7, position 25.
2022-03-24T10:48:19.196 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours50InHours', line 8, position 25.
2022-03-24T10:48:19.197 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours100InHours', line 9, position 26.
2022-03-24T10:48:19.197 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours150InHours', line 10, position 26.
2022-03-24T10:48:19.197 Verbose Could not find member 'orderNumber' on WebApplication1NetCore.Data.OrderEffortDto. Path 'orderNumber', line 11, position 17.
2022-03-24T10:48:19.197 Verbose Could not find member 'withCosts' on WebApplication1NetCore.Data.OrderEffortDto. Path 'withCosts', line 12, position 15.
2022-03-24T10:48:19.199 Info Finished deserializing WebApplication1NetCore.Data.OrderEffortDto. Path '', line 16, position 1.
2022-03-24T10:48:19.200 Verbose Deserialized JSON: 
{
  "effortType": "1",
  "cwe": null,
  "distanceInKilometers": null,
  "effortDate": "2022-03-22T14:45:00+01:00",
  "effortInHours": 1.0,
  "hours25InHours": null,
  "hours50InHours": null,
  "hours100InHours": null,
  "hours150InHours": null,
  "orderNumber": "006001780872",
  "withCosts": false,
  "isNew": true,
  "isEdited": false,
  "isDeleted": false
}

感谢@dbc的评论。我将尝试与转换器在提到的职位Json.net deserialization null guid case,但也将记录发生,以修复根本原因。

更新2

我对转换器做了一些修改,使用了“SwaggerGen.TypeExtensions.GetDefaultValue()",这样我就可以删除泛型,对所有不可为空的类型使用一个转换器。

public class NullToDefaultConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        var defaultValue = objectType.GetDefaultValue();
        return defaultValue != null;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);
        if (token.Type == JTokenType.Null)
             // here I will add a logger to get all faulty calls
             return objectType.GetDefaultValue();
        return token.ToObject(objectType); // Deserialize using default serializer
    }

    // Return false I don't want default values to be written as null
    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
jaxagkaj

jaxagkaj1#

这是一个很好的问题,我最近在将一个大型(错误缠身的)遗留代码库从.NET Framework迁移到.NET Core时也遇到了这个问题。
OP的更新非常有帮助,但是我想分享一个稍微简化的、性能更高的解决方案,它消除了对SwaggerGen.TypeExtensions.GetDefaultValue()的依赖,也没有泛型依赖;因此可以将其作为Converter全局应用:

private static readonly NullToDefaultConverter _jsonLegacyCompatibleNullValueConverter = new();

   . . .

var model = JsonConvert.DeserializeObject(content, modelType, _jsonLegacyCompatibleNullValueConverter);

当Json .NET反序列化时,通过object existingValue输入参数提供当前现有值或默认值,该参数将具有属性的默认值或属性初始值设定项设置的初始值。在这两种情况下,我们可能都希望保留该值,因此可以只返回它,从而提供与旧的.NET Framework Web API行为更兼容的行为(如观察员的帖子所述)。
还要注意,我们只需要处理那些 * 不能被分配 * null值的属性,因此可以在Type上进行更高性能的检查(改编自优雅的stack overflow answer here)。

/// <summary>
/// This is a Json Converter that restores legacy ASP.Net MVC compatible behavior for handling values that cannot be assigned Null.
/// Historically (before .NET Core) Json values of null would result in errors when setting them into values that cannot be assigned null,
///     however these errors treated as warnings and were skipped, leaving the original default values set on the Model. Now in 
///     Asp .NET Core, these failures result in Exceptions being thrown.
/// While previously the result was that these fields were simply left their default values; either the default of the Value type 
///     or the default set in the Property Initializer.
/// Therefore this Json Converter restores this behavior by assigning the Default value safely without any errors being thrown.
/// </summary>
public class NullToDefaultConverter : JsonConverter
{
    public static bool CanTypeBeAssignedNull(Type type)
        => !type.IsValueType || (Nullable.GetUnderlyingType(type) != null);

    //Only Handle Fields that would fail a Null assignment and requires resolving of a non-null existing/default value for the Type!
    public override bool CanConvert(Type objectType) => !CanTypeBeAssignedNull(objectType);

    public override object ReadJson(JsonReader reader, Type objectType, object existingOrDefaultValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);
        return token.Type == JTokenType.Null ? existingOrDefaultValue : token.ToObject(objectType);
    }

    // Return false; we want normal Json.NET behavior when serializing...
    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}

为了进一步扩展如何在全局范围内恢复遗留兼容行为,我们实现了一个自定义的Input Formatter,如下所示:

public class JsonLegacyCompatibilityInputFormatter : TextInputFormatter
    {
        public bool EnableExceptions { get; set; }

        private static readonly NullToDefaultConverter _jsonLegacyCompatibleNullValueConverter = new();

        public JsonLegacyCompatibilityInputFormatter(bool enableExceptions = true)
        {
            EnableExceptions = enableExceptions;
            //Add Supported Media Types
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
            //Add Supported Encodings
            SupportedEncodings.Add(Encoding.UTF8);
            SupportedEncodings.Add(Encoding.Unicode);
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
        {
            var httpRequest = context.HttpContext.Request;
            var modelType = context.ModelType;
            try
            {
                using var streamReader = new StreamReader(httpRequest.Body, effectiveEncoding);
                var content = await streamReader.ReadToEndAsync().ConfigureAwait(false);

                //NOTE: There is a deviation between .NET Framework and Asp .NET Core handling of null values with properties that cannot be assigned a null value.
                //      Therefore To be compatible with Legacy Web API behavior we  provide the JsonLegacyCompatibleNullValueConverter to ensure
                //          that properties that cannot be assigned null from null Json values retain their original or default value without exceptions being thrown.
                var model = JsonConvert.DeserializeObject(content, modelType, _jsonLegacyCompatibleNullValueConverter);

                return await InputFormatterResult.SuccessAsync(model).ConfigureAwait(false);
            }
            catch (Exception exc)
            {
                var message = $"Error occurred during Model binding de-serialization of [{modelType.Name}].";
                serviceProvider.GetService<ILogger>()?.LogError(exc, message);

                #if DEBUG
                Debug.WriteLine(exc.ConvertExceptionToJson());
                #endif

                if (EnableExceptions)
                    throw new Exception(message, exc);
                else
                    return await InputFormatterResult.FailureAsync().ConfigureAwait(false);
            }
        }
    }

通过以下方式配置为供所有模型绑定使用:

// Configure MVC Services...
builder.Services
    .AddControllersWithViews(options =>
    {
        options.InputFormatters.Insert(0, new JsonLegacyCompatibilityInputFormatter());
    })

EDIT -优化版本现已在Nuget上共享:

自从最初分享这个解决方案以来,我意识到有一些性能影响和一些其他的兼容性问题,比如错误处理,需要解决。实作自订JsonBufferedHttpRequestReaderCharArrayPool,而非以不必要的配置序列化至字串。它还通过将所有错误推入ModelState并防止抛出这些异常来实现错误处理的传统兼容性。
此完整版本可在Nuget上获得,网址为:https://github.com/cajuncoding/AspNetCoreMigrationShims
并且可以像Newtonsoft一样通过以下方式轻松配置:

builder.Services
   .AddControllersWithViews(options => {})
   .AddNewtonsfotJson(options => {}) //Original Newtsonsoft Configuration
   .WithNewtonsoftJsonNetFrameworkCompatibility(); //Now updated and re-configured for better Legacy Compatibility
ccrfmcuu

ccrfmcuu2#

仅使属性可为空

public class OrderEffortDto
{
    .........
    public bool? Cwe { get; set; }
    public decimal? EffortInHours { get; set; }
    public decimal? Hours25InHours { get; set; }
    public decimal? Hours50InHours { get; set; }
    public decimal? Hours100InHours { get; set; }
    public decimal? Hours150InHours { get; set; }
   
}

也可以添加构造函数而不是使其可为null。只能在构造函数中包含在反序列化过程中需要更改的属性

[Newtonsoft.Json.JsonConstructor]
     public OrderEffortDto(
     bool? cwe ,
     decimal? effortInHours ,
     decimal? hours25InHours ,
     decimal? hours50InHours ,
     decimal? hours100InHours ,
     decimal? hours150InHours )
    {
        Cwe = cwe==null?false: (bool) Cwe;
        EffortInHours = effortInHours==null? 0: (decimal) effortInHours;
       .... and so on
    }

相关问题