Plexdata Settings Serialization

Overview

The Plexdata Settings Serialization represents an alternative to the standard settings serialization mechanism.

Main feature of this library is that users are able to define the path as well as the extension of their settings file.

As extension to the standard settings handling, this library allows to put the settings into the directory of the executable. This is very interesting for so called Portable Apps, which usually manage all their data within one and the same directory.

Another feature of this library is the possibility to control the format of the settings file. At the moment, reading and writing of settings files in JSON format as well as in XML format is supported. Other formats like for example old‑school INI files may follow in a later version.

Licensing

The software has been published under the terms of

MIT License

Copyright © 2023 plexdata.de

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Installation

The binary files of the Plexdata Settings Serialization are provided as NuGet package and can be obtained from https://www.nuget.org/packages/Plexdata.Settings.Serialization. How to install this NuGet package manually is explained there.

Using the Plexdata Settings Serialization together with Visual Studio.

Additionally, all releases can be downloaded from GitHub. Please visit page Plexdata Settings Serialization to find all available versions.

Introduction

Visual Studio provides a very powerful handling of managing application settings. But unfortunately it is not really possible to load and save settings that are located in the same folder where the executing assembly is located. This makes it almost impossible to write so‑called Portable Apps, which usually manage all operations within one and the same directory.

Another issue with the standard Visual Studio settings management is the fact that controlling the name of the settings file as well as the format of the settings file is also not really possible.

All these features will become possible with this package.

Locations

In this section please find an overview of possible storage locations and how to affect them.

DefaultLocation
The default storage location. In this case the folder of ExecutableFolder is used.
ExecutableFolder
The folder of the executing assembly. This folder is used only if writing files is permitted. In case of writing is not permitted the folder referenced by FallbackLocation is taken instead.
FallbackLocation
The fallback storage location. This folder is used in case of writing files into chosen folder is not permitted. The folder of LocalApplicationData is used for this purpose.
LocalApplicationData
The local application data folder. The resulting path should look like: <drive>\Users\<username>\AppData\Local\<company>\<product>\[<version>\]<filename>.<extension>
ApplicationData
The standard application data folder. The resulting path should look like: <drive>\Users\<username>\AppData\Roaming\<company>\<product>\[<version>\]<filename>.<extension>
CommonApplicationData
The common application data folder. The resulting path should look like: <drive>\ProgramData\<company>\<product>\[<version>\]<filename>.<extension>

The variables <company> and <product> in the above example paths come from the executing assembly. The <version> instead is optional and must be explicitly enabled via property IsVersionized of ISettingsOptions. See below from where these path parts are taken in detail.

<company>
The company descriptor comes either from the Assembly Company Attribute or (if not exist) is taken from the first part of the namespace of the declaring type of the entry point of the executing assembly. This is usually the namespace of class Program.
<product>
The product descriptor comes either from the Assembly Product Attribute or (if not exist) represents the file name of the executing assembly but without its file extension.
<version>
The version descriptor comes either from the Assembly Version Attribute or (if not exist) from the Assembly File Version Attribute . The version descriptor 0.0.0.0 is taken in case of none of these attributes exists.
<filename>
The filename descriptor is nothing else but the name of the executing assembly but without its extension.
<extension>
The extension descriptor comes from property Extension of ISettingsOptions. Its default value is .conf if it hasn’t be changed explicitly.

Models

Models or better data models are user defined classes that are used as generic parameter for interfaces ISettingsReader and ISettingsWriter.

Because of the internal usage of Newtonsoft’s Json.NET package it becomes possible to tag all own model properties with attribute JsonProperty. But to use this feature it might be necessary to include this package in an own project. But keep in mind, the Plexdata Settings Serialization works as well if this package is not manually included in user projects.

Especially for XML serialization another attribute of Newtonsoft’s Json.NET package is used by Plexdata Settings Serialization. This is attribute JsonObject. By using this attribute users can modify the name of the root object within the resulting XML file.

Below find an example model that can be used together with Plexdata Settings Serialization and which tags its properties with attributes of Newtonsoft’s Json.NET package.

[JsonObject("program-settings")]
public class ProgramSettings
{
    [JsonProperty("application-title")]
    public String Title { get; set; }

    [JsonProperty("main-window-position")]
    public Point Location { get; set; }

    [JsonProperty("main-window-size")]
    public Size Dimension { get; set; }
}

Reading

In this section users may find an interesting example of how to read a settings file. The example implementation below assumes the existence of data model ProgramSettings from section above.

class Program
{
    static void Main(String[] args)
    {
        var options = SettingsFactory.Create<ISettingsOptions>(
            SettingsPattern.JsonPattern, StorageLocation.ExecutableFolder, "program.settings");

        var reader = SettingsFactory.Create<ISettingsReader<ProgramSettings>>();

        if (!reader.TryRead(options, out ProgramSettings settings))
        {
            settings = new ProgramSettings();
        }
    }
}

As shown above, an instance of ISettingsOptions is created and configured as first. This configuration assumes a JSON settings file that is located within the directory of the executable file. Furthermore, this settings file uses a file extension of program.settings.

As next an instance of ISettingsReader is created. Thereafter it is tried to read the settings file and, if it fails, a new instances of user defined ProgramSettings is created manually.

Writing

In this section users may find an interesting example of how to write a settings file. The example implementation below assumes the existence of the same data model ProgramSettings as used in section above.

class Program
{
    static void Main(String[] args)
    {
        var options = SettingsFactory.Create<ISettingsOptions>(
            SettingsPattern.JsonPattern, StorageLocation.ExecutableFolder, "program.settings");

        ProgramSettings settings = new ProgramSettings();

        var writer = SettingsFactory.Create<ISettingsWriter<ProgramSettings>>();

        writer.TryWrite(options, settings);
    }
}

As shown above, an instance of ISettingsOptions is created and configured as first. This configuration assumes a JSON settings file that has to be saved into the directory of the executable file. Furthermore, this settings file uses a file extension of program.settings.

As next an instance of ISettingsWriter is created. Thereafter it is tried to write the settings file.

DI‑Handling

The handling of dependency injection is indeed a common issue and is discussed in this section. Additionally note, for the example code in here package Unity is used as dependency injection container.

The first code snippet below shows a typical extension method for interface IUnityContainer to be used to perform all needed dependency injection registrations.

public static IUnityContainer PerformRegistration(this IUnityContainer container)
{
    if (container is null)
    {
        return container;
    }

    container.RegisterFactory<ISettingsOptions>("ProgramSettingsOptions",
        x => SettingsFactory.Create<ISettingsOptions>(
            SettingsPattern.JsonPattern, StorageLocation.ExecutableFolder, "program.settings"
        ));

    container.RegisterFactory<ISettingsReader<ProgramSettings>>("ProgramSettingsReader",
        x => SettingsFactory.Create<ISettingsReader<ProgramSettings>>());

    container.RegisterFactory<ISettingsWriter<ProgramSettings>>("ProgramSettingsWriter",
        x => SettingsFactory.Create<ISettingsWriter<ProgramSettings>>());

    container.RegisterSingleton<IProgramSettingsManager, ProgramSettingsManager>();

    return container;
}

The first three calls exemplarily show how to configure the factory registrations. Furthermore, each of the factory registrations uses a special factory name just to ensure the uniqueness of all ProgramSettings depended classes.

The last call instead registers a Singleton that must be implemented by user code. Managing all settings related tasks is the job of this class. Furthermore, this manager class gets all settings related instances via constructor injection. See next code snippet for such an example implementation.

internal class ProgramSettingsManager : IProgramSettingsManager
{
    private readonly ISettingsOptions options = null;
    private readonly ISettingsReader<ProgramSettings> reader = null;
    private readonly ISettingsWriter<ProgramSettings> writer = null;
    private ProgramSettings instance = null;

    public ProgramSettingsManager(
        [Dependency("ProgramSettingsOptions")] ISettingsOptions options,
        [Dependency("ProgramSettingsReader")] ISettingsReader<ProgramSettings> reader,
        [Dependency("ProgramSettingsWriter")] ISettingsWriter<ProgramSettings> writer)
    {
        this.options = options ?? throw new ArgumentNullException(nameof(options));
        this.reader = reader ?? throw new ArgumentNullException(nameof(reader));
        this.writer = writer ?? throw new ArgumentNullException(nameof(writer));

        this.Instance = new ProgramSettings();
    }

    public ProgramSettings Instance { get; private set; }

    public Boolean Load()
    {
        if (this.reader.TryRead(this.options, out ProgramSettings result))
        {
            this.Instance = result;
            return true;
        }

        return false;
    }

    public Boolean Save()
    {
        return this.writer.TryWrite(this.options, this.Instance);
    }
}

In contrast to constructor injection, the way of property injection would be possible as well. The code snippet below demonstrates it.

internal class ProgramSettingsManager : IProgramSettingsManager
{
    public ProgramSettingsManager()
    {
        this.Instance = new ProgramSettings();
    }

    [Dependency("ProgramSettingsOptions")]
    public ISettingsOptions Options { private get; set; }

    [Dependency("ProgramSettingsReader")]
    public ISettingsReader<ProgramSettings> Reader { private get; set; }

    [Dependency("ProgramSettingsWriter")]
    public ISettingsWriter<ProgramSettings> Writer { private get; set; }

    public ProgramSettings Instance { get; private set; }

    public Boolean Load()
    {
        if (this.Reader.TryRead(this.Options, out ProgramSettings result))
        {
            this.Instance = result;
            return true;
        }

        return false;
    }

    public Boolean Save()
    {
        return this.Writer.TryWrite(this.Options, this.Instance);
    }
}

Limitation

Not known at the moment.

Known Issues

Not known at the moment.