Configuring Applications with Configuration Files

The most common method for configuring .NET applications is with json files.

dotnet new console -lang c# -n FunWithConfiguration -o .\FunWithConfiguration -f net6.0
dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration
dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration.Binder
dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration.Json

This adds a reference for configuration subsystem, the json file-based .NET configuration subsystem, and the binding extensions for configuration into your project.

Start by adding a new JSON file into your project named "appsettings.json".

<ItemGroup>
        <None>
                <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        </None>
</ItemGroup>

Finally, update appsettings.json

{
"CarName":"Suzy"
}

The final step to adding the configuration into your app is to read in the configuration file.

IConfiguration config = new ConfigurationBuilder()
                             .SetBasePath(Directory.GetCurrentDirectory())
                             .AddJsonFile("appsettings.json", true, true)
                             .Build();
Console.WriteLine(config["CarName"]);

If the request name doesn't exist in the configuration, the result will be null.

Console.WriteLine(config["c"]);

There is also a GetValue() method (and its generic version GetValue<T>()) that can retrieve primitive values from the configuration.

Console.WriteLine(config.GetValue(typeof(string), "CarName"));
Console.WriteLine(config.GetValue<string>("CarName"));

These methods return the default value (e.g. null for reference types, 0 for numeric types) if the name requested doesn't exist.

Console.WriteLine(config.GetValue<int>("CarName2")); // 0

These methods will throw an exception if the value found for the name can't be intrinsically cast to the requested type.

try
 {
     Console.WriteLine(config.GetValue<int>("CarName")); ;
 }
catch (Exception ex)
 {
     Console.WriteLine(ex.Message);
 }

Note: The GetValue() method as designed to work with primitive types. For complex types, use the Bind() or Get()/Get<T>() methods.

Multiple Configuration Files

More than one configuration file can be added into the configuration system.

add -> appsettings.development.json

Update project file


  <ItemGroup>
          <None Update="appsettings.development.json">
                  <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          </None>
  </ItemGroup>

IConfiguration config = new ConfigurationBuilder()
                             .SetBasePath(Directory.GetCurrentDirectory())
                             .AddJsonFile("appsettings.json", true, true)
                             .AddJsonFile("appsettings.development.json", true, true)
                             .Build();

Working with Objects

{
"CarName":"Suzzy",
"Car":{
"Make":"Honda",
"Color":"Blue",
                "PetName":"Dad's Taxi",
           }
}

To access multilevel JSON values, the key used for searching is the hierarchy of JSON with each level separated by colons (:).

Console.WriteLine(config["Car:Color"]);

Instead of traversing the hierarchy of names, entire sections can be retrieved using the GetSection() method.

IConfigurationSection s = config.GetSection("Car");
Console.WriteLine(s["Color"]);

As a final note for working with objects, you can use the Bind() method to bind configuration values to an existing instance of an object or the Get() method.

Car c = new Car();
s.Bind(c);
Console.WriteLine(c.Color);

The Get() method creates a new instance of the specified type from a section of the configuration. The non-generic version of the method returns an object type, so the return value must be cast to the specific type before being used.

var carFromGet = config.GetSection(nameof(Car)).Get(typeof(Car)) as Car;
Console.WriteLine(carFromGet.Color);

If the named section is not found, the Get() method returns null:

config.GetSection("Car2").Get(typeof(Car));

The generic version returns an instance of the specified type without having to perform a cast.

var c2 = config.GetSection(nameof(Car)).Get<Car>();

The Bind() and Get()/Get<T>() methods use reflection to match the names of the publish properties on the class to the names in the configuration section in a case-insensitive manner. This still works:

{
   "CarName": "Suzu",
   "Car": {
     "Make": "Honda",
     "Color": "Blue",
     "PetName": "Dad's Taxi"
   }
 }

If a property in the configuration doesn't exist in the class (or the name is spelled differently), then that particular configuration value (by default is ignored.

{
   "CarName": "Suzu",
   "Car": {
     "Make": "Honda",
     "Color": "Blue",
     "PetNameColor": "Dad's Taxi"
   }
 }

The Bind(),Get() and Get<T>() methods can optionally take an Action<BinderOptions> to further refine the process of updating (Bind()) or instantiating (Get()/Get<T>()) class instances:

public class BinderOptions
 {
     public bool BindNonPublicProperties { get; set; } //Defaults to false
     public bool ErrorOnUnknownConfiguration { get; set; } //Defaults to false
 }

If the ErrorOnUnknownConfiguration option is set to true, then an InvalidOperationException will be

thrown if the configuration contains a name that doesn’t exist on the model.

try
 {
     _ = config.GetSection(nameof(Car)).Get<Car>(t => t.ErrorOnUnknownConfiguration = true);
 }
catch (InvalidOperationException ex)
 {
     Console.WriteLine($"An exception occurred: {ex.Message}");
 }

The other option allows for binding non-public properties. If non-publish properties should be bound from the configuration, set BindNonPublicProperties.

var carFromGet3 = config.GetSection(nameof(Car)).Get<Car>(t => t.BindNonPublicPropert
 ies = true);

New in C# 10, the GetRequiredSection() method will throw an exception if the section is not configured.

try
 {
     config.GetRequiredSection("Car2").Bind(notFoundCar);
 }
catch (InvalidOperationException ex)
 {
     Console.WriteLine($"An exception occurred: {ex.Message}");
 }

Building and Consuming a .NET Class Library

  1. Create a reusable DLL, refence it from another project, use it.
  2. The fundamental way to share code in .NET

public = visible to consumers,

internal = Hidden (default)

// STEP 1 - Create class Library
// dotnet new classlib -n CarLibrary -f net6.0
namespace CarLibrary
 {
     public class SportsCar // Consumers can see this
     {
         public string Name { get; set; }
         public void Turbo()
         {
             Console.WriteLine("Zoom");
         }
     }
     internal class EngineHelper // Consumers cannot see this
     {
         internal static void Fix()
         {
             Console.WriteLine("Fixed");
         }
     }
 }
 
// STEP 2 - Build => dotnet build -c Release -> CarLibrary.dll
// STEP 3 - Create Consumer => dotnet new console -n CarClient -f net6.0
// STEP 4 - Add reference => dotnet add reference ..\CarLirary\CarLibrary.csproj
// STEP 5 - Use it
using CarLibrary;
 SportsCar s = new();
s.Name = "Unper";
s.Turbot();
// EngineHelper.Fix(); // Error internal, not visible.

Exploring the Manifest

ildasm /METADATA /out=CarLibrary.il .\CarLibrary.dll

The manifest section of the disassembled results starts with // Metadata version : 4.0.30319.

Each .assembly extern block is qualified by the .publickeytoken and .ver directives.

The .publickeytoken instruction is present only if the assembly has been configured with a strong name. The .ver token defines the numerical version identifier of the referenced assembly.

Another way to add the metadata to your assembly is directly in the *.csproj project file.

<PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <RootNamespace>CustomNamespaces</RootNamespace>
        <Copyright>Copyright 2021</Copyright>
        <Authors>Phil Japikse</Authors>
        <Company>Apress</Company>
        <Product>Pro C# 10.0</Product>
        <PackageId>CarLibrary</PackageId>
        <Description>This is an awesome library for cars.</Description>
        <AssemblyVersion>1.0.0.1</AssemblyVersion>
        <FileVersion>1.0.0.2</FileVersion>
        <Version>1.0.3</Version>
</PropertyGroup>

Exploring the CIL

An assembly does not contain platform-specific instructions; rather it contains platform-agnostic Common Intermediate Language (CIL) instructions. When the .NET Runtime loads an assembly into memory, the underlying CIL is compiled (using the JIT compiler) into instructions that can be understood by the target platform.

Building a C# Client Applications

  1. Create a console app that uses your class library.

using CarLibrary;
 SportsCar sc = new SportsCar("Viper");
sc.Turbo();
sc.PrintState();
 Minivan mv = new Minivan("Family Bus");
mv.Turbo();
mv.PrintState();
// Polymorphism works across assemblies:
 Car[] cars = { sc, mv };
foreach(Car c in cars)
 {
     c.Turbo();
 }

Exposing Internal Types to Other Assemblies

internal = hidden from everyone outside assembly.

But sometimes you need to expose it 2 ways to do it.

// WAY 1 - Assembly Attribute
// In CarLibrary, add to any .cs file (usually AssemblyInfo.cs)
using System.Runtime.CompilerServices;
 [assembly: InternalsVisibleTo("CarLibrary.Tests")]

// WAY 2 - Project File (.csproj)
// In CarLibrary.csproj:
<ItemGroup>
        <InternalsVisibleTo Include="CarLibrary.Tests"/>
</ItemGroup>

Same result, different place to write it.