.Net Core Services

This is a review of some of the core internals of the .Net Core Framework. Sample code can be found here:

https://github.com/johnlee/dotnetcoreservices

 

Background

NET was first announced by Microsoft in 2000 and then evolved from there. The .NET Framework has been the primary .NET implementation produced by Microsoft during that nearly two decade period.

The major differences between .NET Core and the .NET Framework:

  • App-models — .NET Core does not support all the .NET Framework app-models. In particular, it doesn’t support ASP.NET Web Forms and ASP.NET MVC, but it supports ASP.NET Core MVC. It was announced that .NET Core 3 will support WPF and Windows Forms.
  • APIs — .NET Core contains a large subset of .NET Framework Base Class Library, with a different factoring (assembly names are different; members exposed on types differ in key cases). These differences require changes to port source to .NET Core in some cases (see microsoft/dotnet-apiport). .NET Core implements the .NET Standard API specification.
  • Subsystems — .NET Core implements a subset of the subsystems in the .NET Framework, with the goal of a simpler implementation and programming model. For example, Code Access Security (CAS) is not supported, while reflection is supported.
  • Platforms — The .NET Framework supports Windows and Windows Server while .NET Core also supports macOS and Linux.
  • Open Source — .NET Core is open source, while a read-only subset of the .NET Framework is open source.

While .NET Core is unique and has significant differences to the .NET Framework and other .NET implementations, it is straightforward to share code between these implementations, using either source or binary sharing techniques.

Because .NET Core supports side-by-side installation and its runtime is completely independent of the .NET Framework, it can be installed on machines with .NET Framework installed without any issues.

Comparison with Mono

Mono is the original cross-platform and open source .NET implementation, first shipping in 2004. It can be thought of as a community clone of the .NET Framework. The Mono project team relied on the open .NET standards (notably ECMA 335) published by Microsoft in order to provide a compatible implementation.

The major differences between .NET Core and Mono:

  • App-models — Mono supports a subset of the .NET Framework app-models (for example, Windows Forms) and some additional ones (for example, Xamarin.iOS) through the Xamarin product. .NET Core doesn’t support these.
  • APIs — Mono supports a large subset of the .NET Framework APIs, using the same assembly names and factoring.
  • Platforms — Mono supports many platforms and CPUs.
  • Open Source — Mono and .NET Core both use the MIT license and are .NET Foundation projects.
  • Focus — The primary focus of Mono in recent years is mobile platforms, while .NET Core is focused on cloud and desktop workloads.

 

.NET Standard

.NET Standard is a specification of .NET APIs available to all .NET implementations to interface with lower level infrastructure. The formal description is as below:

The motivation behind the .NET Standard is establishing greater uniformity in the .NET ecosystem. ECMA 335 continues to establish uniformity for .NET implementation behavior, but there’s no similar spec for the .NET Base Class Libraries (BCL) for .NET library implementations.

The .NET Standard enables the following key scenarios:

  • Defines uniform set of BCL APIs for all .NET implementations to implement, independent of workload.
  • Enables developers to produce portable libraries that are usable across .NET implementations, using this same set of APIs.
  • Reduces or even eliminates conditional compilation of shared source due to .NET APIs, only for OS APIs.

The .NET Standard specification is a standardized set of APIs. The specification is maintained by .NET implementors, specifically Microsoft (includes .NET Framework, .NET Core, and Mono) and Unity. A public feedback process is used as part of establishing new .NET Standard versions through GitHub.

An easy example showing the differences between .Net Standard vs .Net Core is by looking at the core dependency libraries.

  

 

.Net Core default file structure

  • Program.cs – this is the main executable class and entry point for the application. It will define things related to the host, such as Configurations and then call the app builder which is often done through a Startup.cs class.
  • Startup.cs – this is the main startup definition for the application. This filename does not need to be “Startup”, just what is used by default in the .net core template solution. This class will defines services, the request pipeline (routing), and configure any other middleware required by the app (in the Startup.Configure method)
  • x.csproj = has information about the project, including dependency libraries from Nuget.
  • appsettings.json – default configuration file. We can change these.

 

Environment

ASP.NET Core reads the environment variable ASPNETCORE_ENVIRONMENT at app startup and stores the value in IHostingEnvironment.EnvironmentName. You can set ASPNETCORE_ENVIRONMENT to any value, but three values are supported by the framework: DevelopmentStaging, and Production. If ASPNETCORE_ENVIRONMENT isn’t set, it defaults to Production.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Staging_2"))
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();
    app.UseMvc();
}

 

Configuration

App configuration in ASP.NET Core is based on key-value pairs established by configuration providers. Configuration providers read configuration data into key-value pairs from a variety of configuration sources:

  • Azure Key Vault
  • Command-line arguments
  • Custom providers (installed or created)
  • Directory files
  • Environment variables
  • In-memory .NET objects
  • Settings files

Configuration packages for common configuration provider scenarios are included in the Microsoft.AspNetCore.App metapackage. Code examples that follow and in the sample app use the Microsoft.Extensions.Configuration namespace:

using Microsoft.Extensions.Configuration;

 

A common mistake in development is storing sensitive information in Configurations – such as connection strings to data sources. These should not be stored in areas such as config text files or source control. Instead, use features such as environment variables and user-secrets to store these types of sensitive data.

 

In the Program.cs class we can define Configurations. It is automatically injected into the Startup constructor.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile(
                "config.json", optional: true, reloadOnChange: true);
        })
        .UseStartup();

 

 

Logging

.Net Core comes with a logger built into it’s core services under Microsoft.Extensions.Logger. This can be configured to log to console, file, database, etc. It is configured in the as part of the WebHostBuilder (Program.cs) so that it can be used immediately in the Startup class. We need to define a Provider for the logger.

A logging provider displays or stores logs. For example, the Console provider displays logs on the console, and the Azure Application Insights provider stores them in Azure Application Insights. Logs can be sent to multiple destinations by adding multiple providers. The default log provider is Console.

public static void Main(string[] args)
{
    var webHost = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var env = hostingContext.HostingEnvironment;
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", 
                      optional: true, reloadOnChange: true);
            config.AddEnvironmentVariables();
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole();
            logging.AddDebug();
            logging.AddEventSourceLogger();
        })
        .UseStartup()
        .Build();

    webHost.Run();
}

Since the logger is configured at the HostBuilder, it can be used during the application startup in the Startup.cs class.

public class Startup
{
    private readonly ILogger _logger;

    public Startup(IConfiguration configuration, ILogger logger)
    {
        Configuration = configuration;
        _logger = logger;
    }
...

ILogger also has the following features:

  • Log Level – indicates the severity of the log, Info, Warning, Error, etc
  • Log Category – indicates where the log occurred, by default being the class name where the log event happened, ex “MyApp.Controllers.HomeController”
  • Event Id
  • Message Template
  • Exceptions (the actual exception thrown can be directly passed into the logger)
  • Filtering
  • Scopes

By default the .Net Core framework provides Console, Debug, Event, Azure and AppInsight logging. There are many other third party logging providers to log into databases, queues or file system.

 

Unit Testing

Functional tests are expensive. They typically involve opening up the application and performing a series of steps that you (or someone else), must follow in order to validate the expected behavior. Unit tests, on the other hand, take milliseconds, can be run at the press of a button and do not necessarily require any knowledge of the system at large. Unit testing also protects against regression defects and therefore regression testing. With unit testing, it’s possible to rerun your entire suite of tests after every build or even after you change a line of code. Giving you confidence that your new code does not break existing functionality.

Unit test best practices:

  • Fast. It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run. Milliseconds.
  • Isolated. Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • Repeatable. Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs.
  • Self-Checking. The test should be able to automatically detect if it passed or failed without any human interaction.
  • Timely. A unit test should not take a disproportionately long time to write compared to the code being tested. If you find testing the code taking a large amount of time compared to writing the code, consider a design that is more testable.

 

When unit testing we need to create mocks, fakes and/or stubs. These differ in the following:

Fake – A fake is a generic term which can be used to describe either a stub or a mock object. Whether it is a stub or a mock depends on the context in which it’s used. So in other words, a fake can be a stub or a mock.

Mock – A mock object is a fake object in the system that decides whether or not a unit test has passed or failed. A mock starts out as a Fake until it is asserted against.

Stub – A stub is a controllable replacement for an existing dependency (or collaborator) in the system. By using a stub, you can test your code without dealing with the dependency directly. By default, a fake starts out as a stub.

 

Unit test names should follow these guidelines:

  • The name of the method being tested.
  • The scenario under which it’s being tested.
  • The expected behavior when the scenario is invoked.
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
  var stringCalculator = new StringCalculator(); // Arrange
  var actual = stringCalculator.Add("0"); // Act
  Assert.Equal(0, actual); // Assert
}

Inside the unit test, remember to follow these steps:

  • Arrange your objects, creating and setting them up as necessary.
  • Act on an object.
  • Assert that something is as expected.

 

 

References

.Net Core
https://docs.microsoft.com/en-us/dotnet/core/

.NET Standard
https://docs.microsoft.com/en-us/dotnet/standard/net-standard

.Net Core 2.0
https://channel9.msdn.com/Events/dotnetConf/2017/T321

.Net Standard Deep Dive
https://channel9.msdn.com/Shows/On-NET/NET-Standard-Deep-Dive

Logging to File
https://andrewlock.net/creating-a-rolling-file-logging-provider-for-asp-net-core-2-0/

Unit Testing
https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing?view=aspnetcore-2.2