1

Background:
I am creating an avaloniaUI project and in it I have a json config with a class that serializes and deserializes it on initialization and editing. If the file can't be found on initialization the class makes a new one with a list of default properties. This class is supposed to be the only part of the code that can do that.

Problem:
For some reason the config file, when it does not exist already, is created before the constructor call of the config serializer class. That lead my friends and I to think that the file was being created somewhere else in the project. That can't be the case though because I've used a control f tool to comb the entire project for references of any filestreams, create commands, or a reference to the config path and I found nothing else that's capable of generating a file other than the code that's supposed to create the config but never runs. So what happens is the file is created empty before the code that's supposed to handle generation is called thus skipping the proper generation code which leads to json loading an empty string when the file is deserialized and it creates null value exceptions.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Newtonsoft.Json;

namespace RefOrganizerAvalonia.Util
{
    public class ConfigSerializer
    {
        public List<ConfigItem> Items { get; }
        
        private string _configPath;
        
        private static List<ConfigItem> BuildConfig()
        {
            return new List<ConfigItem>()
            {
                new ConfigItem("PageResultSize", 50, typeof(int))
                ,new ConfigItem("ResultsPerLine", 5, typeof(int))
            };
        }

        private List<ConfigItem> LoadConfig()
        {
            using (FileStream fs = new FileStream(_configPath, FileMode.Open))
            {
                var sr = new StreamReader(fs);
                var dataStr = sr.ReadToEnd();
                return JsonConvert.DeserializeObject(dataStr) as List<ConfigItem>;
            }
        }

        public ConfigSerializer()
        { //break point here reveals that even before any of the constructor code is executed the 
          //file is created from an unknown location
            var folder = Environment.SpecialFolder.LocalApplicationData;
            var path = Environment.GetFolderPath(folder);
            _configPath = Path.Join(path, "ref_organizer_config.json");
            Debug.WriteLine(_configPath);
            Debug.WriteLine(File.Exists(_configPath));
            if (!File.Exists(_configPath)) //never accessed because the (empty) file already 
                                           //exists by this point despite no code that can create 
                                           //it being executed
            {
                Debug.WriteLine("Config Not Found");
                //Save BuildConfig() results in json string and write to file
                Items = BuildConfig();
                SaveConfig();
            }
            else
            {
                Items = LoadConfig();
            }
        }

        private void SaveConfig()
        {
            using (FileStream fs = new FileStream(_configPath, FileMode.OpenOrCreate))
            {
                Debug.WriteLine("Save Config Triggered");
                var data = JsonConvert.SerializeObject(Items);
                Debug.WriteLine($"Serialized Config: {data}");
                var sw = new StreamWriter(fs);
                sw.Write(data);
            }
        }
    }
}

Tests Performed:

  1. Changing the call order and location of the config serializer class.

    Result: Empty file with config name and path is still created right before the initialization of config serializer.

  2. Changing the path of the _configPath variable.

    Result: Empty file created under new name.

  3. Setting SaveConfig() function to private.

    Result: No change.

Note:

If you need additional context of the avalonia project code for whatever reason send me a comment and I will post it ASAP.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • 3
    If you comment out the code that's supposed to handle generation and then rebuild the project, does it still create the file? – Andrew Morton Feb 03 '22 at 18:23
  • What we could really do with is the code for a *minimal* example that demonstrates the problem. Cut huge swathes of code out of your project, retesting after every change, until either you've found the problem or you can present us with code that demonstrates the problem. – Jon Skeet Feb 03 '22 at 18:24
  • @AndrewMorton I just tested that on your suggestion and no it doesn't generate the file when the generation code is cut out of the file. – Artemis Venatrix Feb 03 '22 at 18:27
  • @ArtemisVenatrix So, you need to find out why that code is being called before you expect. [How can I find the method that called the current method?](https://stackoverflow.com/questions/171970/how-can-i-find-the-method-that-called-the-current-method) – Andrew Morton Feb 03 '22 at 18:34
  • @AndrewMorton The issue is that my debug readout doesn't display the first call to that function for some reason. I wonder if its being called before debug is initialized. – Artemis Venatrix Feb 03 '22 at 18:51
  • @ArtemisVenatrix What if you put an explicit `throw new Exception("Oops!")` in there? And have you checked it is doing a debug build as you want? – Andrew Morton Feb 03 '22 at 18:58
  • _”The issue is that my debug readout doesn't display the first call to that function for some reason.”_ … Why would you think the function is being called? If you have a breakpoint on the first line of code IN that function, and the breakpoint IN that function is NOT getting hit… then the function was NOT called. What is mysterious about this? – JohnG Feb 03 '22 at 19:00
  • @JohnG its mysterious because despite not calling the only function in the programing that should be able to create the config file, the file is still created – Artemis Venatrix Feb 03 '22 at 19:06
  • @AndrewMorton Ive tried putting break points in save config just now and the dont get hit despite the fact that when the function is commented out the file isnt created. I'll try throwing an exception next. – Artemis Venatrix Feb 03 '22 at 19:08
  • @AndrewMorton I've just discovered that when an exception throw precludes the using statement everything works as expected. I'm going to attempt to rewrite it without a using statement. – Artemis Venatrix Feb 03 '22 at 19:12
  • Then it should be obvious that the file is somehow getting created somewhere else. Since you state that you have looked all through the code and did not find anything, then are you sure you are looking for the right command? I am just saying, that the “created” file may be created though some other mechanism which you are NOT necessarily looking for. I suggest you delete the file, and KEEP the file explorer window open, then walk through the code. With each step, check to see if the file is created, this should help you isolate when/where the file is getting erroneously created. – JohnG Feb 03 '22 at 19:16
  • Does this answer your question? [c# using statement and StreamWriter](https://stackoverflow.com/questions/16514530/c-sharp-using-statement-and-streamwriter) – Charlieface Feb 03 '22 at 22:06

1 Answers1

1

In my small tests tracing your code, it appears the problem lies in the SaveConfig() method. Walking through the code should reveal that the line of code… sw.Write(data); … is executing, however, nothing is actually written to the file. Adding a Flush statement for the StreamWriter should ensure that… sw.Write(data); … gets done. Adding the Flush() command to the writer appears to actually write the data to the file.

private void SaveConfig() {
  using (FileStream fs = new FileStream(_configPath, FileMode.OpenOrCreate)) {
    Debug.WriteLine("Save Config Triggered");
    var data = JsonConvert.SerializeObject(Items);
    Debug.WriteLine($"Serialized Config: {data}");
    var sw = new StreamWriter(fs);
    sw.Write(data);
    sw.Flush();
  }
}

Edit... StreamWriter should be wrapped in a using statement.

Further tests reveal that wrapping a Using statement around the StreamWriter is needed and should be implemented. Therefore, wrapping the StreamWriter in a using statement should Flush the data for you. Below is a better approach with the using statement.

private void SaveConfig() {
  using (FileStream fs = new FileStream(_configPath, FileMode.OpenOrCreate)) {
    Debug.WriteLine("Save Config Triggered");
    var data = JsonConvert.SerializeObject(Items);
    Debug.WriteLine($"Serialized Config: {data}");
    using (var sw = new StreamWriter(fs)) {
      sw.Write(data);
    }
  }
}
JohnG
  • 9,259
  • 2
  • 20
  • 29