In all types of software, we need to have a configuration file to be able to modify values between different kind of environments. The actual process of changing configuration files in Xamarin will be covered in an upcoming post. In this post we will focus on how to add and read a configuration file.
Configuration file
We want to have the following Json configuration file on three different platforms :
{
"BaseUrl": "https://www.bing.com"
}
In order to be able to change it in a continuous deployment system, this file will be defined on each platform specific projects. These files will need to be declared as in the following :
Shared code
In our shared project we define a Configuration folder with the following files :
- Configuration
- IConfigurationManager
- IConfigurationStreamProvider
- IConfigurationStreamProviderFactory
- ConfigurationManager
Configuration model
This is the model representing the configuration file.
public class Configuration
{
public string BaseUrl { get; set; }
}
Configuration manager interface
This is the interface for the configuration manager in case you use (and you should) dependency injection approach.
public interface IConfigurationManager
{
Task<Configuration> GetAsync(CancellationToken cancellationToken);
}
Configuration stream provider interface
This is the contract for classes providing a stream to the configuration file. This implements IDisposable and will be defining per platforms.
public interface IConfigurationStreamProvider : IDisposable
{
Task<Stream> GetStreamAsync();
}
Configuration stream provider factory interface
This is the contract for the factories building the implementations of the configuration stream provider interface. The implementations will be platform specific.
public interface IConfigurationStreamProviderFactory
{
IConfigurationStreamProvider Create();
}
Configuration manager
This is the actual implementation of the configuration manager.
Static initialization
For the sake of simplicity, this implementation is a singleton but I highly encourage you to use dependency injection on your projects.
public class ConfigurationManager : IConfigurationManager
{
private static IConfigurationStreamProviderFactory _factory;
private Configuration _configuration;
public static IConfigurationManager Instance { get ; } = new ConfigurationManager();
public static void Initialize(IConfigurationStreamProviderFactory factory)
{
_factory = factory;
}
...
}
You can see that this class implements a static initialization method like a lot of Xamarin plugins. This pattern is easy to use in projects where dependency injection is unfortunately not in place. We use this method to get implementations of the configuration stream provider factory for the current platform.
Instance initialization
private readonly SemaphoreSlim _semaphoreSlim;
private bool _initialized;
protected ConfigurationManager()
{
_semaphoreSlim = new SemaphoreSlim(1, 1);
}
private async Task InitializeAsync(CancellationToken cancellationToken)
{
if (_initialized)
return;
try
{
await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
if (_initialized)
return;
var configuration = await ReadAsync().ConfigureAwait(false);
_initialized = true;
_configuration = configuration;
}
finally
{
_semaphoreSlim.Release();
}
}
As we want our implementation to be thread safe, we use a SemaphoreSlim to protect the code where the configuration file is read. We then keep the configuration instance in memory.
Configuration parsing
private async Task<Configuration> ReadAsync()
{
using (var streamProvider = _factory.Create())
using (var stream = await streamProvider.GetStreamAsync().ConfigureAwait(false))
{
var configuration = Deserialize<Configuration>(stream);
return configuration;
}
}
private T Deserialize<T>(Stream stream)
{
if (stream == null || !stream.CanRead)
return default(T);
using (var sr = new StreamReader(stream))
using (var jtr = new Newtonsoft.Json.JsonTextReader(sr))
{
var js = new Newtonsoft.Json.JsonSerializer();
var value = js.Deserialize<T>(jtr);
return value;
}
}
Reading the file is a pretty straightforward process, first get the stream and the parse it with whatever parser you want, here JSON.NET.
Interface implementation
public async Task<Configuration> GetAsync(CancellationToken cancellationToken)
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
if (_configuration == null)
throw new InvalidOperationException("Configuration should not be null");
return _configuration;
}
As the initialization method returns immediately if the configuration has already been parsed we can just invoke it everytime and return the value of the configuration field to the caller.
Platform specific code
In each platforms we have to implement the configuration stream provider and the configuration stream provider factory and initialize the configuration manager.
UWP
Configuration stream provider
public class UwpConfigurationStreamProvider : IConfigurationStreamProvider
{
private IRandomAccessStreamWithContentType _inputStream;
private Stream _readingStream;
private const string ConfigurationFilePath = "ms-appx:///Assets/config.json";
public async Task<Stream> GetStreamAsync()
{
ReleaseUnmanagedResources();
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(ConfigurationFilePath));
_inputStream = await file.OpenReadAsync();
_readingStream = _inputStream.AsStreamForRead();
return _readingStream;
}
private void ReleaseUnmanagedResources()
{
_inputStream?.Dispose();
_readingStream?.Dispose();
_inputStream = null;
_readingStream = null;
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
~UwpConfigurationStreamProvider()
{
ReleaseUnmanagedResources();
}
}
Please note that we need to correctly implement the IDisposable interface.
Configuration stream provider factory
public class UwpConfigurationStreamProviderFactory : IConfigurationStreamProviderFactory
{
public IConfigurationStreamProvider Create()
{
return new UwpConfigurationStreamProvider();
}
}
Configuration manager initialization
ConfigurationManager.Initialize(new UwpConfigurationStreamProviderFactory());
Add this line in the App.xaml.cs file, in the OnLaunched method.
iOS
Configuration stream provider
public class IOSConfigurationStreamProvider : IConfigurationStreamProvider
{
private const string ConfigurationFilePath = "Assets/config.json";
private Stream _readingStream;
public Task<Stream> GetStreamAsync()
{
ReleaseUnmanagedResources();
_readingStream = new FileStream(ConfigurationFilePath, FileMode.Open, FileAccess.Read);
return Task.FromResult(_readingStream);
}
private void ReleaseUnmanagedResources()
{
_readingStream?.Dispose();
_readingStream = null;
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
~IOSConfigurationStreamProvider()
{
ReleaseUnmanagedResources();
}
}
Please note that we need to correctly implement the IDisposable interface.
Configuration stream provider factory
public class IOSConfigurationStreamProviderFactory : IConfigurationStreamProviderFactory
{
public IConfigurationStreamProvider Create()
{
return new IOSConfigurationStreamProvider();
}
}
Configuration manager initialization
ConfigurationManager.Initialize(new IOSConfigurationStreamProviderFactory());
Add this line in the AppDelegate.cs file, in the FinishedLaunching method.
Android
Configuration stream provider
public class AndroidConfigurationStreamProvider : IConfigurationStreamProvider
{
private const string ConfigurationFilePath = "config.json";
private readonly Func<Context> _contextProvider;
private Stream _readingStream;
public AndroidConfigurationStreamProvider(Func<Context> contextProvider)
{
_contextProvider = contextProvider;
}
public Task<Stream> GetStreamAsync()
{
ReleaseUnmanagedResources();
AssetManager assets = _contextProvider().Assets;
_readingStream = assets.Open(ConfigurationFilePath);
return Task.FromResult(_readingStream);
}
private void ReleaseUnmanagedResources()
{
_readingStream?.Dispose();
_readingStream = null;
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
~AndroidConfigurationStreamProvider()
{
ReleaseUnmanagedResources();
}
}
Please note that we need to correctly implement the IDisposable interface and get an instance to the Android Context.
Configuration stream provider factory
public class AndroidConfigurationStreamProviderFactory : IConfigurationStreamProviderFactory
{
private readonly Func<Context> _contextProvider;
public AndroidConfigurationStreamProviderFactory(Func<Context> contextProvider)
{
_contextProvider = contextProvider;
}
public IConfigurationStreamProvider Create()
{
return new AndroidConfigurationStreamProvider(_contextProvider);
}
}
Please note that we need to get an instance to the Android Context to pass it at the configuration stream provider.
Configuration manager initialization
ConfigurationManager.Initialize(new AndroidConfigurationStreamProviderFactory(() => this));
In the case of a Xamarin Forms application, put this code inside MainActivity.cs file, in the OnCreate method.
Using the configuration manager
To illustrate the usage of these classes, I have created a Xamarin Forms sample project on GitHub.
In the end using the configuration manager is pretty simple :
using (var cts = new CancellationTokenSource())
{
// Create or get a cancellation token from somewhere
var config = await ConfigurationManager.Instance.GetAsync(cts.Token);
var baseUrl = config.BaseUrl;
// Use the configuration value
}
Conclusion
Of course this is only a sample project approach. In a more complex one, we should get the configuration manager inside a view model through dependency injection.
Please remember to use async await correctly which means avoiding async void and using cancellation tokens.
As always, please feel free to read my previous posts and to comment below, I will be more than happy to answer.
Comments