简体   繁体   中英

JsonConvert.DeserializeObject works in debug build but crashes in release build

My Winforms app stores settings in a JSON file which the following class manages. It runs fine when the application is built in debug mode but crashes in release mode with "Object reference not set to an instance of an object." The code that crashes is JsonConvert.DeserializeObject in the GetSettings method.

  • Visual Studio 2022
  • .Net 4.8
  • Newtonsoft 13.0.2
using Newtonsoft.Json;

namespace MyApp
{
    public sealed class Settings
    {
        [JsonProperty]
        public Guid ApplicationId { get; private set; }

        [JsonProperty]
        public string AccessToken { get; private set; }

        private static string fileName = String.Empty;
        private static string FileName
        {
            get
            {
                if ( String.IsNullOrEmpty( fileName ) )
                {
                    fileName = Path.Combine( Folder, "settings.json" );
                }
                return fileName;
            }
        }

        private static string folder = String.Empty;
        private static string Folder
        {
            get
            {
                if ( String.IsNullOrEmpty( folder ) )
                {
                    folder = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData ), "Company Name", "Program Name" );
                    try
                    {
                        var directory = Directory.CreateDirectory( folder );
                    }
                    catch ( Exception x )
                    {
                        Debug.WriteLine( $"Unable to create settings folder: {folder}" );
                        throw;
                    }
                }
                return folder;
            }
        }

        private static Settings instance = null;

        private Settings()
        {
        }

        public static Settings GetSettings()
        {
            if ( instance == null )
            {
                string settingsText = File.ReadAllText( FileName, Encoding.UTF8 );
                instance = JsonConvert.DeserializeObject<Settings>( settingsText );
            }
            return instance;
        }
    }
}

The partial stack trace shows that internally the crash occurs in Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject when calling the Settings ctor:

   at MyApp.Settings()
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader reader, JsonObjectContract objectContract, JsonProperty containerMember, JsonProperty containerProperty, String id, Boolean& createdFromNonDefaultCreator)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at MyApp.Settings.GetSettings()
...

ILSpy decompiles the Newtonsoft API to the following but I haven't found it useful for finding the crash. How can I determine where and why the crash is occurring, and why only in release mode?

// Newtonsoft.Json.Serialization.JsonSerializerInternalReader
public object CreateNewObject(JsonReader reader, JsonObjectContract objectContract, [Nullable(2)] JsonProperty containerMember, [Nullable(2)] JsonProperty containerProperty, [Nullable(2)] string id, out bool createdFromNonDefaultCreator)
{
    object obj = null;
    if (objectContract.OverrideCreator != null)
    {
        if (objectContract.CreatorParameters.Count > 0)
        {
            createdFromNonDefaultCreator = true;
            return this.CreateObjectUsingCreatorWithParameters(reader, objectContract, containerMember, objectContract.OverrideCreator, id);
        }
        obj = objectContract.OverrideCreator(CollectionUtils.ArrayEmpty<object>());
    }
    else if (objectContract.DefaultCreator != null && (!objectContract.DefaultCreatorNonPublic || this.Serializer._constructorHandling == ConstructorHandling.AllowNonPublicDefaultConstructor || objectContract.ParameterizedCreator == null))
    {
        obj = objectContract.DefaultCreator();
    }
    else if (objectContract.ParameterizedCreator != null)
    {
        createdFromNonDefaultCreator = true;
        return this.CreateObjectUsingCreatorWithParameters(reader, objectContract, containerMember, objectContract.ParameterizedCreator, id);
    }
    if (obj != null)
    {
        createdFromNonDefaultCreator = false;
        return obj;
    }
    if (!objectContract.IsInstantiable)
    {
        throw JsonSerializationException.Create(reader, "Could not create an instance of type {0}. Type is an interface or abstract class and cannot be instantiated.".FormatWith(CultureInfo.InvariantCulture, objectContract.UnderlyingType));
    }
    throw JsonSerializationException.Create(reader, "Unable to find a constructor to use for type {0}. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute.".FormatWith(CultureInfo.InvariantCulture, objectContract.UnderlyingType));
}

It's not clear why Json.NET is throwing trying to construct your Settings object in release mode. Possibilities might include:

  • Your framework has IL pruning enabled, as suggested by Marc Gravell .
  • In your release build you are running under partial trust and do not have permission to access private constructors via reflection.

Whatever the problem is, you can work around it easily by allocating the Settings object yourself and populating it using JsonSerializer.Populate() .

In addition, you have a second problem that could be symptomatic only in release builds: you are not initializing your static singletons Settings.instance , Settings.FileName and Settings.Folder in a thread-safe manner. For an explanation of why, see Implementing the Singleton Pattern in C# by Jon Skeet. That article recommends using System.Lazy<T> to implement lazy, thread-safe initialization.

Putting all that together, your Settings can be modified as follows:

public sealed class Settings
{
    private Settings() { }

    public static Settings GetSettings() { return instance.Value; }

    private static Lazy<Settings> instance = 
        new Lazy<Settings>(() =>
                            {
                                var settings = new Settings();
                                using (var textReader = new StreamReader(SettingsPath.Instance.FullName, Encoding.UTF8 ))
                                {
                                    JsonSerializer.CreateDefault().Populate(textReader, settings);
                                }
                                
                                return settings;
                            });

    // Remainder of Settings unchanged
    
    [JsonProperty]
    public Guid ApplicationId { get; private set; }

    [JsonProperty]
    public string AccessToken { get; private set; }
}

internal sealed class SettingsPath
{
    // Nested global singleton, used for lazy thread-safe initialization of settings directory.  
    private SettingsPath(string fullName) { this.FullName = fullName; }
    
    public static SettingsPath Instance { get { return instance.Value; } }

    static Lazy<SettingsPath> instance = 
        new Lazy<SettingsPath>(() =>
                               {
                                   var folder = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData ), "Company Name", "Program Name" );
                                   try
                                   {
                                       var directory = Directory.CreateDirectory( folder );
                                   }
                                   catch ( Exception x )
                                   {
                                       Debug.WriteLine( string.Format("Unable to create settings folder: {0}", folder ));
                                       throw;
                                   }
                                   var fileName = Path.Combine( folder, "settings.json" );
                                   return new SettingsPath(fileName);
                               });
    
    public string FullName { get; private set; } // You could save the directory and file name as well, if necessary
}

Notes:

  • Using Populate() looks cleaner in my opinion. Settings is supposed to be a singleton, and in this version of the code the Settings object is shown to be constructed only once, from within the class itself.

  • When deserializing from a file, Newtonsoft recommends deserializing directly from a stream for performance reasons. See Performance Tips: Optimize Memory Usage for details.

  • I extracted the settings directory into its own lazily initialized SettingsPath singleton because it looked cleaner, and in case you need access to the settings directory elsewhere in your code.

Demo fiddle here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM