Nett is a library that helps to read and write TOML files in .Net.
- 'Nett' also allows you to specify timespan values. Timespan currently isn't supported in the original 'TOML' spec. The following TimeSpan formats are supported:
- hh:mm
- hh:mm:ss
- hh:mm:ss.ff
- dd.hh:mm:ss
- dd.hh:mm:ss.ff
Install it via NuGet:
Package | Nett | Nett.Coma |
---|---|---|
Standard | ||
With Strong Name | n/a |
All common 'TOML' operations are performed via the static class Nett.Toml
. Although there are other
types available from the library in general using that single type should be sufficient
for most standard scenarios.
The following example shows how you can write and read some complex object to/from a 'TOML' file. The object that gets serialized and deserialized is defined as follows:
public class Configuration
{
public bool EnableDebug { get; set; }
public Server Server { get; set; }
public Client Client { get; set; }
}
public class Server
{
public TimeSpan Timeout { get; set; }
}
public class Client
{
public string ServerAddress { get; set; }
}
To write the above object to a 'TOML' File you have to do:
var config = new Configuration()
{
EnableDebug = true,
Server = new Server() { Timeout = TimeSpan.FromMinutes(1) },
Client = new Client() { ServerAddress = "http://127.0.0.1:8080" },
};
Toml.WriteFile(config, "test.tml");
This will write the following content to your hard disk:
EnableDebug = true
[Server]
Timeout = 00:01:00
[Client]
ServerAddress = "http://127.0.0.1:8080"
To read that back into your object you need to:
var config = Toml.ReadFile<Configuration>("test.tml");
If you only have a 'TOML' file but no corresponding class that the data in the 'TOML' file maps to, you can read the data into a generic TomlTable structure and extract a member the like:
TomlTable table = Toml.ReadFile("test.tml");
var timeout = table.Get<TomlTable>("Server").Get<TimeSpan>("Timeout");
In advanced use cases 'Nett' behavior needs to be tweaked. This can be achieved by providing custom configuration information. Currently there are two ways to modify the behavior of 'Nett'.
- Attributes that get applied on target objects and their properties
- A custom configuration object passed to Read/Write methods
A TomlTable
is always associated with a configuration object. This association
is established via the Read
and Create
methods. If no config is specified
during a read/create the default config will be used. There are overloads of
these methods that allow to associate a custom configuration. Once a table
is associated with a custom configuration this association cannot be changed
for that table instance.
Also for the Write
operation a config object can be specified. But, that configuration
will not get associated with the table. It will only be used temporary
during the write operation. If no config is specified for the write operation,
the table associated config will be used to perform all write operations.
To create a new configuration do the following:
var myConfig = TomlConfig.Create();
This will create a copy of the default configuration. The copy can be modified via a fluent configuration API.
The following sections will show how this API can be used to support various use cases.
If your type doesn't have a default constructor or is not constructible (interface or abstract class) 'Nett' will not be able to deserialize into that type without some help.
Assume we have the following type, that extends the configuration class from the basic examples:
public class ConfigurationWithDepdendency : Configuration
{
public ConfigurationWithDepdendency(object dependency)
{
}
}
When you try to deserialize the test.tml
into that type via
var config = Toml.ReadFile<ConfigurationWithDepdendency>("test.tml");
you will get the following exception:
Failed to create type 'ConfigurationWithDepdendency'. Only types with a parameterless constructor or an specialized creator can be created. Make sure the type has a parameterless constructor or a configuration with an corresponding creator is provided.
To make this work, we need to pass a custom configuration to the read method that tells 'Nett', how the type can be created. This is done the by:
var myConfig = TomlConfig.Create(cfg => cfg
.ConfigureType<ConfigurationWithDepdendency>(ct => ct
.CreateInstance(() => new ConfigurationWithDepdendency(new object()))));
var config = Toml.ReadFile<ConfigurationWithDepdendency>("test.tml", myConfig);
'Nett' defines the following standard conversion sets that be activated/deactivated via a 'TOML' config.
- NumericalSize
Only conversions between floating point and integral data types are disallowed. All other conversions are allowed, also the ones where the target type could be to small to hold the source value e.g. TomlInt -> char. - Serialize
- Enum <-> TomlString
- Guid <-> TomlString
- NumericalType
Also allow conversion between floating point and integral data types e.g. TomlFloat -> char.
By default the 'NumericalSize' and 'Serialize' sets are activated. All possible conversions that Nett can do can be activated by:
var config = TomlConfig.Create(cfg => cfg
.AllowImplicitConversions(TomlConfig.ConversionSets.All));
var tbl = Toml.ReadString("f = 0.99", config);
var i = tbl.Get<int>("f"); // i will be '0'
This example shows the drawbacks of activating all conversions. Here the read int
will have a value of 0. The next write would write value 0
into the TOML file and
so probably change the type of the config value. Simply explained, the more conversion are
enabled, the higher the risk is that subtle bugs are introduced.
The opposite route is to disable all 'Nett' implicit conversion via
var config = TomlConfig.Create(cfg => cfg
.AllowImplicitConversions(TomlConfig.ConversionSets.None));
var tbl = Toml.ReadString("i = 1", config);
// var i = tbl.Get<int>("i"); // Would throw InvalidOperationException as event cast from TomlInt to int is not allowed
var i = tbl.Get<long>("i"); // Only long will work, no other type
The drawback of this approach is that your objects are only allowed to use TOML native types to work without further casting or custom converters.
Any set combination can be activated by logical combination of the set flags e.g.:
var config = TomlConfig.Create(cfg => cfg
.AllowImplicitConversions(TomlConfig.ConversionSets.NumericalType | TomlConfig.ConversionSets.Serialize));
Var various scenarios a logical combination of the default conversion sets with some custom converters may be the best choice.
'TOML' has a very limited set of supported types. Assume you have some very simple CLR type
used for config root object called TableContainingMoney
:
public struct Money
{
public string Currency { get; set; }
public decimal Ammount { get; set; }
public static Money Parse(string s) => new Money() { Ammount = decimal.Parse(s.Split(' ')[0]), Currency = s.Split(' ')[1] };
public override string ToString() => $"{this.Ammount} {this.Currency}";
}
public class TableContainingMoney
{
public Money NotSupported { get; set; }
}
With the default configuration 'Nett' would produce the following 'TOML' content
[NotSupported]
Currency = "EUR"
[NotSupported.Ammount]
This not very useful content is generated because of
- 'Nett' treats Money as a complex type and therefore writes it as a table
- 'Nett' cannot handle the
decimal
type by default.
Reading back this generated 'TOML' will not produce the same data structure as during the write.
To fix this we can try to tell Nett how to handle the decimal
type correctly.
In this use case we decide we don't care about precision and just write is
as a TomlFloat that has double precision.
var obj = new TableContainingMoney()
{
NotSupported = new Money() { Ammount = 9.99m, Currency = "EUR" }
};
var config = TomlConfig.Create(cfg => cfg
.ConfigureType<decimal>(type => type
.WithConversionFor<TomlFloat>(convert => convert
.ToToml(dec => (double)dec)
.FromToml(tf => (decimal)tf.Value))));
var s = Toml.WriteString(obj, config);
var read = Toml.ReadString<TableContainingMoney>(s, config);
Now 'Nett' will produce the following output:
[NotSupported]
Currency = "EUR"
Ammount = 9.99
This is already a lot better and will read back the correct data structure. But in our case the money type itself can serialize it to a string. So it's functionality can be used to store the information more efficiently and not as a TomlTable (complex data structure) by using a different converter telling 'Nett' how to handle the 'Money' type itself rather than it's components.
var obj = new TableContainingMoney()
{
NotSupported = new Money() { Ammount = 9.99m, Currency = "EUR" }
};
var config = TomlConfig.Create(cfg => cfg
.ConfigureType<Money>(type => type
.WithConversionFor<TomlString>(convert => convert
.ToToml(custom => custom.ToString())
.FromToml(tmlString => Money.Parse(tmlString.Value)))));
var s = Toml.WriteString(obj, config);
var read = Toml.ReadString<TableContainingMoney>(s, config);
Using this custom configuration will produce the following TOML which is more efficient and readable.
NotSupported = "9.99 EUR"
Also the deserialization will work because the conversion specified both directions (FromlToml & ToToml). It is not required to always specify both conversion directions. E.g. if you only write TOML files, the 'FromToml' part could be omitted.
By default TOML reads/writes all public properties of an CLR object. In some cases it may be required that 'Nett' doesn't do so for a property for various reasons.
The following class outlines such a scenario
public sealed class Computed
{
public int X { get; set; } = 1;
public int Y { get; set; } = 2;
public int Z => X + Y;
}
Serializing an instance of this class will produce the following TOML content
X = 1
Y = 2
Z = 3
The instance is serializeable but deserializing this content into a Computed Instance will
fail with and message that will be something like Property set method not found
, because
the computed property only has a getter, but no setter.
So the computed Z
property needs to be ignored. This can be achieved in two ways:
- Via fluent configuration
var c = new Computed();
var config = TomlConfig.Create(cfg => cfg
.ConfigureType<Computed>(type => type
.IgnoreProperty(o => o.Z)));
var w = Toml.WriteString(c, config);
var r = Toml.ReadString<Computed>(w, config);
- Applying a
TomlIgnore
attribute onto the computed property
public sealed class Computed
{
public int X { get; set; } = 1;
public int Y { get; set; } = 2;
[TomlIgnore]
public int Z => X + Y;
}
Using the fluent configuration API has the benefit, that the CLR object doesn't need to be modified.
'Nett.Coma' is an extension library for Nett. The purpose of 'Coma' is to provide a modern and powerful config system in the .Net ecosytem.
Since .Net 2.0 there exists a .Net framework integrated configuration system inside the System.Configuration
namespace.
So why a new config system? Because of the following disadvantages, that the .Net integrated config system has:
- XML as configuration format
- Very much boilerplate code needed to get strongly typed config objects
- No way to support for advanced use case scenarios (e.g. multi file configurations)
- Not actively maintained
'Coma' attempts to solve many of these pitfalls by providing the following features:
- TOML used as the configuration format
- Coma wraps plain CLR config type to provide additional functionality
- Out of the box support for multi file / merge configurations (e.g. like Git config system)
Assume the following settings object is used inside an app
public class AppSettings
{
public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(15);
public UserSettings User { get; set; } = new UserSettings();
public class UserSettings
{
public string UserName { get; set; }
}
}
The following example should give an idea how to integrate the 'Coma' system to get a application settings implementation.
var appSettings = "%APPDATA%/AppSettings.toml";
var userSettings = "%USERDATA%/UserSettings.toml";
// Merge only works when files exist on disk, user has to do the initial creation manually
File.WriteAllText(appSettings, "IdleTimeout = 00:15:00");
File.WriteAllText(userSettings,
@"
[User]
UserName = ""Test""
");
// Prepare sources for merging
var appSource = ConfigSource.CreateFileSource(appSettings);
var userSource = ConfigSource.CreateFileSource(userSettings);
var merged = ConfigSource.Merged(appSource, userSource); // order important here
// merge both TOML files into one settings object
var settings = Config.Create(() => new AppSettings(), merged);
// Read the settings
var oldTimeout = settings.Get(s => s.IdleTimeout);
var oldUserName = settings.Get(s => s.User.UserName);
// Save settings. When no override source is given, the system will save back to the file
// where the setting was loaded from during the merge operation
settings.Set(s => s.User.UserName, oldUserName + "_New");
// Save setting into user file. User setting will override app setting until the setting
// gets cleared from the user file
settings.Set(s => s.IdleTimeout, oldTimeout + TimeSpan.FromMinutes(15), userSource);
// Now clear the user setting again, after that the app setting will be returned when accessing the setting again
settings.Clear(s => s.IdleTimeout, userSource);
// Now clear the setting without a source, this will clear it from the currently active one.
// In this case the setting will be cleared from the app file => The setting will not be in any config anymore
settings.Clear(s => s.IdleTimeout);
XXXX-XX-XX: v0.7.0 (TOML 0.4.0)
Nett:
- Fix: Write key back to file with same type #23
2017-04-20: v0.6.3 (TOML 0.4.0)
Nett:
- Fix: Serialize
uint
correctly #16
Coma:
- Fix: Type conversions #17
2016-12-11: v0.6.2 (TOML 0.4.0) Nett:
- Fix: Ignore static properties #15
2016-10-22: v0.6.1 (TOML 0.4.0) Nett:
- Fix: Array of tables serialization #14
2016-10-12: v0.6.0 (TOML 0.4.0)
Nett:
- Add: Properties of TOML mapped classes can be ignored via attribute or config
- Add: TomlTable supports Freezable pattern
- Fix: All parser errors include line and column
- Fix: Various invalid TOML cases now cause a parser error as expected
- Removed: Comments merge mode (will be redesigned and added in future version)
Coma:
- Initial release
2016-08-14: v0.5.0 (TOML 0.4.0)
- Changed: Configuration API to have clearer syntax and behavior
- Add: implicit cast sets; Guids and Enums are converted automatically
- Fix: Weird formatting and new lines for nested tables
- Fix: Invalid TOML strings produce better parser error message
2016-04-10: v0.4.2 (TOML 0.4.0)
- Fix: Float was written as TomlInt when it had no decimal places #8
- Fix: Inline tables read as arrays #7
- Fix: Integer bare keys not working #6
2016-02-14: v0.4.1 (TOML 0.4.0)*
- Add: Support for short date time formats
- Fix: Writing files is culture invariant
- Fix: Table encoding/decoding when they are used inside table arrays
2015-12-18: v0.4.0 (compatible with TOML 0.4.0) First public preview release