JSON format has been a standard used amongst many different framework and languages for quite a few years now. It’s so cool, that even the .NET Core team have decided to include it in its framework which results in e.g. being able to store the application settings within a JSON file, which is much more human readable and less bloated than the old one App or Web.config written using the XML.
In today’s post, I’d like to present how easy it is to create your own JSON configuration reader and move the application settings to such file.
To begin with, let’s define our goal – we want to have a single config.json file in order to store the separate settings within the sections in this file. And of course, we want this file to be automatically mapped to the corresponding C# classes and later on injected using some IoC framework. We will create a very simple and naive console application, that tries to connect to the database and create a new user account using the provided email and password.
This would be our configuration:
1 2 3 4 5 6 7 8 9 10 |
{ "database": { "connectionString": "Data Source= .\\SqlExpress; Integrated Security=True;", "timeoutSeconds": 5 }, "user": { "defaultUsername": "user", "activeByDefault": true } } |
And the classes:
1 2 3 4 5 6 7 8 9 10 11 |
public class DatabaseSettings { public string ConnectionString { get; protected set; } public int TimeoutSeconds { get; protected set; } } public class UserSettings { public string DefaultUsername { get; protected set; } public bool ActiveByDefault { get; protected set; } } |
Before we dive into the actual implementation of the ISettingsReader, let me just paste the code that will try to mimic some business logic. Let’s start with the user entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class User { public string Email { get; } public string Username { get; } public string Password { get; } public bool IsActive { get; } public User(string email, string username, string password, bool isActive) { Email = email; Username = username; Password = password; IsActive = isActive; } public override string ToString() => $"Email: {Email}, Username: {Username}, Password: {Password}, Is active: {IsActive}"; } |
Next, focus on the database provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public interface IDatabase { void OpenConnection(); IEnumerable<User> GetAllUsers(); void SaveUser(User user); } public class Database : IDatabase { private static readonly IList<User> Users = new List<User> { new User("user1@test.com", "user1", "test", true), new User("user2@test.com", "user2", "test", false), new User("user3@test.com", "user1", "test", true) }; private readonly DatabaseSettings _databaseSettings; public Database(DatabaseSettings databaseSettings) { _databaseSettings = databaseSettings; } public void OpenConnection() { Console.WriteLine("Connecting to the database using connection string: " + $"'{_databaseSettings.ConnectionString}'."); } public IEnumerable<User> GetAllUsers() => Users; public void SaveUser(User user) { Users.Add(user); Console.WriteLine($"Saving user: '{user.Email}' to the database."); } } |
And finally, the application service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public interface IUserService { User Register(string email, string password); } public class UserService : IUserService { private readonly IDatabase _database; private readonly UserSettings _userSettings; public UserService(IDatabase database, UserSettings userSettings) { _database = database; _userSettings = userSettings; } public User Register(string email, string password) { _database.OpenConnection(); var userExists = _database .GetAllUsers() .Any(x => x.Email.Equals(email, StringComparison.InvariantCultureIgnoreCase)); if (userExists) throw new Exception($"User with email: '{email}' already exists."); var user = new User(email, _userSettings.DefaultUsername, password, _userSettings.ActiveByDefault); _database.SaveUser(user); return user; } } |
Ok, that was the necessary code just to make this example something a little bit more than just a single interface and its implementation. So, here comes the part that you’ve been waiting for, just make sure that you install the Newtonsoft.Json NuGet package first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
public interface ISettingsReader { T Load<T>() where T : class, new(); T LoadSection<T>() where T : class, new(); object Load(Type type); object LoadSection(Type type); } public class SettingsReader : ISettingsReader { private readonly string _configurationFilePath; private readonly string _sectionNameSuffix; private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings { ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, ContractResolver = new SettingsReaderContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; public SettingsReader(string configurationFilePath, string sectionNameSuffix = "Settings") { _configurationFilePath = configurationFilePath; _sectionNameSuffix = sectionNameSuffix; } public T Load<T>() where T : class, new() => Load(typeof(T)) as T; public T LoadSection<T>() where T : class, new() => LoadSection(typeof(T)) as T; public object Load(Type type) { if (!File.Exists(_configurationFilePath)) return Activator.CreateInstance(type); var jsonFile = File.ReadAllText(_configurationFilePath); return JsonConvert.DeserializeObject(jsonFile, type, JsonSerializerSettings); } public object LoadSection(Type type) { if (!File.Exists(_configurationFilePath)) return Activator.CreateInstance(type); var jsonFile = File.ReadAllText(_configurationFilePath); var section = ToCamelCase(type.Name.Replace(_sectionNameSuffix, string.Empty)); var settingsData = JsonConvert.DeserializeObject<dynamic>(jsonFile, JsonSerializerSettings); var settingsSection = settingsData[section]; return settingsSection == null ? Activator.CreateInstance(type) : JsonConvert.DeserializeObject(JsonConvert.SerializeObject(settingsSection), type, JsonSerializerSettings); } private class SettingsReaderContractResolver : DefaultContractResolver { protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(p => CreateProperty(p, memberSerialization)) .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(f => CreateProperty(f, memberSerialization))) .ToList(); props.ForEach(p => { p.Writable = true; p.Readable = true; }); return props; } } private static string ToCamelCase(string text) => string.IsNullOrWhiteSpace(text) ? string.Empty : $"{text[0].ToString().ToLowerInvariant()}{text.Substring(1)}"; } |
Really, not too much code as for the simple class that can read the configuration file, ain’t it? And if you don’t want to use the protected accessor, you can skip the ContractResolver part.
We’re almost finished. Let’s plug all of that stuff into some IoC library – I’ll use the Autofac, yet feel free to chose whatever you like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class SettingsModule : Module { private readonly string _configurationFilePath; private readonly string _sectionNameSuffix; public SettingsModule(string configurationFilePath, string sectionNameSuffix = "Settings") { _configurationFilePath = configurationFilePath; _sectionNameSuffix = sectionNameSuffix; } protected override void Load(ContainerBuilder builder) { builder.RegisterInstance(new Settings.SettingsReader(_configurationFilePath, _sectionNameSuffix)) .As<ISettingsReader>() .SingleInstance(); var settings = Assembly.GetExecutingAssembly() .GetTypes() .Where(t => t.Name.EndsWith(_sectionNameSuffix, StringComparison.InvariantCulture)) .ToList(); settings.ForEach(type => { builder.Register(c => c.Resolve<ISettingsReader>().LoadSection(type)) .As(type) .SingleInstance(); }); } } public static class Container { public static IContainer Initialize() { var builder = new ContainerBuilder(); builder.RegisterModule(new SettingsModule("config.json")); builder.RegisterType<Database>().As<IDatabase>(); builder.RegisterType<UserService>().As<IUserService>(); return builder.Build(); } } |
And eventually, the actual Program.cs source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Program { private static readonly IContainer Container = IoC.Container.Initialize(); static void Main(string[] args) { Console.WriteLine("Create new user."); Console.Write("Email: "); var email = Console.ReadLine(); Console.Write("Password: "); var password = Console.ReadLine(); using (var scope = Container.BeginLifetimeScope()) { var userService = scope.Resolve<IUserService>(); var user = userService.Register(email, password); Console.WriteLine($"User has been created. {user}."); } } } |
Run the application now and you will see all of the settings correctly injected into the services. You can use this solution with any other application type as well (Web, WPF etc.).
The only important thing here is to set the config.json file to have a Build Action of type Content and either Copy Always or Copy if newer setting, so that it’s available for the application (or just keep it somewhere else and fix the configuration file path).
Also, if you don’t want to use the sections, you can choose the Load() instead of LoadSection() and have all of the properties stored within a root of the configuration file.
And that’s all – you can download this example here.
Pingback: Storing C# app settings with JSON - How to Code .NET
Pingback: typeof(arunselvakumar); | //Build June 7, 2016
Pingback: The week in .NET – 06/07/2016 | .NET Blog
Pingback: The week in .NET – 06/07/2016 | Tech News
Tak dla ciekawości – HOCON jednak chyba fajniejszy – przemyślany pod konfigurację 🙂
Jedyną styczność jaką z nim miałem to tylko podczas przerabiania kursu Akka.NET ale widzę, że warto się z nim bliżej zapoznać ;).
It’s worth noting that most of this you get for free if you use the IOptions pattern – injecting your ‘DatabaseSettings’ class into a service becomes a one liner in ConfigureServices:
services.Configure(Configuration.GetSection(“Database”));
Your database constructor then becomes:
public Database(IOptions databaseSettings)
{
_databaseSettings = databaseSettings.Value;
}
I have a post about it here: https://andrewlock.net/how-to-use-the-ioptions-pattern-for-configuration-in-asp-net-core-rc2/
Correct, I’m aware of the IOptions interface but it’s only applicable within the .NET Core platform and not everyone uses it yet.
Oops, yes you’re right, I meant to mention that.
Just to be clear, it’s a feature of ASP.NET Core – I believe it requires netstandard1.1, so it can be used in both .NET Core and .NET Framework/mono 4.5+
Nie post:)
Exactly and that’s why I wrote thist post so that devs who are not using .NET Core yet, could implement such feature on their own if they need it :).
Thanks!
Piotr/Andrew,
Thank you for sharing the idea of storing configuration in json, it is very useful.
I have a problem where the User Id and Password store at the configuration file are in plain text.
Do you know how I can overcome this problem?
I want to create c# winform application where I need to connect to database, but at the same time, this need to be deployed to other user. I don’t want the userid and password to be exposed and also I want this to be updatable on the client side.
Likely to change:
Database Server Name, Database Name, User Id and Password.
Thanks in Advance.
Regards Dat.
Hi Dat,
I’m glad that you find this solution to be useful. Speaking of storing credentials, what you can do about it, is to keep them encrypted in the settings file, however, you have to bear in mind that you need to store somewhere an encryption key (e.g. in system’s registry) in order to be able to decrypt the settings.
In order to achieve such goal, you need to create your own ContractResolver and IValueProvider that you can pass to the Newtonsoft.Json settings.
Please take a look at the following sample that I’ve taken from one of my projects – it’s not a 100% solution, as you still need to decrypt the data within this code, but it shouldn’t be too difficult and hopefully will give you the initial idea how to overcome your issue.
https://gist.github.com/spetz/e1567775608e9dbd041137e59cd3be25
If you have any further question, please let me know!
Cheers
Piotr Thank you for very quick respond.
You’re welcome, I hope that this solution will help with solving your problem :).
Thank you. Your solution helped me.
Always happy to help!
hi. what is namespace for IoC ? visul studio says the name “IoC” does not exist.
excuse me. i removed the word “IoC” and changed first “Container” to “Container_”
Ok, so I understand that everything works for you.
Thanks very much indeed for this insight. I used it in a webapi 2 project and even though settings objects just resolved all the same, the property values turn out to be null
yep, it’s happening the same to me. Did you find any solution?
it turns out the problem was with the path. The file json was not being found at runtime and that’s why the properties were null.
Happy to hear that it was a rather simple bug, that you fixed.
Thanks for your example but “storing settings” includes for me the possibility to save settings, which is missing in your example. You can only read with your methods. If you’ve got time you could add the saving part to complete this example.
Piotr this is great I was looking to move some windows registry settings out of a few classes into a JSON file and I had similar ideas.
Thank you for the heads up!
Hi Piotr,
thank you very much for your example. Is there an easy way to convert the usage of Autofac into Prism/Unity?
All of my other modules are using Unity.
I was able to change
builder.RegisterInstance(new Settings.SettingsReader(_configurationFilePath, _sectionNameSuffix))
.As()
.SingleInstance();
into
containerRegistry.RegisterInstance(typeof(ISettingsReader), new SettingsReader(_configurationFilePath, _sectionNameSuffix));
but for
settings.ForEach(type =>
{
builder.Register(c => c.Resolve().LoadSection(type))
.As(type)
.SingleInstance();
});
I’m not sure how to solve this.
And is it possible to have settings for each module to get saved in one file by using:
var settings = Assembly.GetExecutingAssembly()
.GetTypes() ….
Regards,
Athu
Pingback: .NET Blog