Wednesday, September 30, 2020

Microsoft.Extensions.DependencyInjection

I've written posts describing dependency injection via constructors, methods, properties, and events but these are all homegrown implementations and I've ignored Unity, etc until now. Microsoft has introduced a simple-ish dependency injection service specifically designed for .Net Core, but usable elsewhere. I'm going to look at how to implement dependency injection using this Microsoft service using my standard application -> configuration -> data provider -> logging dependency hierarchy.

Usually you know at design time which classes will need to be injected. Although the ServiceCollection allows generic services to be added, you normally know which ones you will be adding as you design your application. In my example the logging dependency needs to be injected into the data provider and the data provider needs to be injected into the configuration.

Microsoft allows you to write extension methods for each of the injectable classes. If an injectable class needs a dependency injected itself, the extension method handles it. This hides implementation details and also relaxes dependencies to some extent. It's not perfect, but it's not bad.

Most of the code in this example is the same as previous examples so I'm just going to put the production code in one big file and highlight the new stuff.

Start a new Visual Studio 2019 C#, .Net Core console project and call it CodeDI. Use the package manager to install Microsoft.Extensions.DependencyInjection and System.Data.SqlClient. I'm going to assume you have SQLServer installed with a master database.

Make program.cs look like the code below. The most interesting part is ServiceProvider and the ServiceExtensions class. The ServiceProvider statement simply creates a collection of dependencies using the two extension methods we put in the ServiceExtensions class. That collection gets passed to the Configuration constructor so it can pull out the data provider we want it to use.

The ServiceExtensions class contains an extension method for each of the two classes we have identified as being injectable. The logging provider extension method adds a new singleton instance of the desired logging provider type (because we only need one instance). The data provider extension method builds the ServiceProvider we have so far and adds a new singleton instance of the desired data provider type with the ServiceProvider as a parameter so the data provider's constructor can pull out all the dependencies it needs. Note we don't have to know which dependencies it needs - just that it needs some.

If we switch the order of the registrations we have a problem because the logging provider won't exist when the data provider needs it. Unless we check for this, we will get some null property exceptions later. Using GetRequiredService would help by raising the exceptions at the correct place in the code.

using System;
using System.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;

namespace CoreDI
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceProvider services = new ServiceCollection()
                .RegisterLoggingProvider<ProductionLoggingProvider>()
                .RegisterDataProvider<ProductionDataProvider>()
                .BuildServiceProvider();

            Configuration configuration = new Configuration(services);
            bool IsProductionMode = configuration.GetSettings().isProductionMode;

            Console.WriteLine($"IsProductionMode is {IsProductionMode}");
            Console.WriteLine($"DataProvider is {configuration.GetDataProviderName()}");
            Console.WriteLine($"LoggingProvider is {configuration.GetLoggingProviderName()}");
            Console.WriteLine($"DataProvider's LoggingProvider is {configuration.GetDataProviderLoggingProviderName()}");
        }
    }

    public static class ServiceExtensions
    {
        public static IServiceCollection RegisterLoggingProvider<T>(this IServiceCollection serviceswhere T:BaseLoggingProvider
        {
            services.AddSingleton(typeof(BaseLoggingProvider), Activator.CreateInstance(typeof(T)));
            return services;
        }

        public static IServiceCollection RegisterDataProvider<T>(this IServiceCollection serviceswhere T:BaseDataProvider
        {
            ServiceProvider serviceProvider = services.BuildServiceProvider();
            services.AddSingleton(typeof(BaseDataProvider), Activator.CreateInstance(typeof(T), serviceProvider));
            return services;
        }
    }

    public abstract class BaseLoggingProvider
    {
        abstract public string GetName();
        abstract public void Log(string s);
    }

    public class ProductionLoggingProvider : BaseLoggingProvider
    {
        public override string GetName()
        {
            return "Production logging provider";
        }
        public override void Log(string s)
        {
            // write to log file
        }
    }

    public class TestLoggingProvider : BaseLoggingProvider
    {
        public override string GetName()
        {
            return "Test logging provider";
        }
        public override void Log(string s)
        {
            Console.WriteLine(s);
        }
    }

    public class Settings
    {
        public bool isProductionMode;
    }

    public abstract class BaseDataProvider
    {
        internal BaseLoggingProvider loggingProvider = null;    // This would normally be protected
        public BaseDataProvider(ServiceProvider services)
        {
            loggingProvider = services.GetService<BaseLoggingProvider>();
        }
        public abstract string GetName();
        public abstract void OpenConnection();
        public abstract Settings GetSettings();
        public abstract void CloseConnection();
        public abstract bool IsConnectionOpen();
    }

    public class ProductionDataProvider : BaseDataProvider
    {
        public ProductionDataProvider(ServiceProvider services) : base(services) { }

        private SqlConnection sqlConnection;

        public override void CloseConnection()
        {
            loggingProvider.Log("Closing connection");
            sqlConnection.Close();
            sqlConnection = null;
        }

        public override string GetName()
        {
            return "Production data provider";
        }

        public override Settings GetSettings()
        {
            loggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { isProductionMode = true };
            else
                throw new Exception("Connection is not open");
        }

        public override bool IsConnectionOpen()
        {
            return (sqlConnection != null && sqlConnection.State == System.Data.ConnectionState.Open);
        }

        public override void OpenConnection()
        {
            loggingProvider.Log("Opening connection");
            sqlConnection = new SqlConnection("server=(local);database=master;trusted_connection=true");
            sqlConnection.Open();
        }
    }

    public class TestDataProvider : BaseDataProvider
    {
        public TestDataProvider(ServiceProvider services) : base(services) { }

        private bool isConnectionOpen = false;

        public override void CloseConnection()
        {
            loggingProvider.Log("Closing connection");
            isConnectionOpen = false;
        }

        public override string GetName()
        {
            return "Test data provider";
        }

        public override Settings GetSettings()
        {
            loggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { isProductionMode = false };
            else
                throw new Exception("Connection is not open");
        }

        public override bool IsConnectionOpen()
        {
            return (isConnectionOpen);
        }

        public override void OpenConnection()
        {
            loggingProvider.Log("Opening connection");
            isConnectionOpen = true;
        }
    }

    public class Configuration
    {
        private BaseDataProvider dataProvider;
        private BaseLoggingProvider loggingProvider;

        public Configuration(ServiceProvider services)
        {
            dataProvider = services.GetService<BaseDataProvider>();
            loggingProvider = services.GetService<BaseLoggingProvider>();
        }

        public string GetDataProviderName()
        {
            return dataProvider.GetName();
        }

        public string GetLoggingProviderName()
        {
            return loggingProvider.GetName();
        }

        public string GetDataProviderLoggingProviderName()
        {
            return dataProvider.loggingProvider.GetName();
        }

        public Settings GetSettings()
        {
            Settings settings;
            dataProvider.OpenConnection();
            settings = dataProvider.GetSettings();
            dataProvider.CloseConnection();
            return settings;
        }
    }
}

If you run the program you will see the expected results.


Now let's see how we test this. Add an NUnit .Net Core, C# project to the solution and call it CoreDITest. Add references to Microsoft.Extensions.DependencyInjection and CoreDI. Rename Test.cs to ConfigurationTest.cs and make the contents look like the code below. It looks very similar to the previous tests we have done. I highlighted the interesting part.

As you can see, the only thing that changed between the production and the test code is the classes we are registering. This is how it should be.

using NUnit.Framework;
using Microsoft.Extensions.DependencyInjection;
using CoreDI;
 
namespace CoreDITest
{
    public class Tests
    {
        [Test]
        public void TestProvidersTest()
        {
            ServiceProvider services = new ServiceCollection()
               .RegisterLoggingProvider<TestLoggingProvider>()
               .RegisterDataProvider<TestDataProvider>()
               .BuildServiceProvider
();
 
            Configuration configuration = new Configuration(services);
            bool IsProductionMode = configuration.GetSettings().isProductionMode;
 
            Assert.IsFalse(IsProductionMode);
            Assert.AreEqual("Test data provider"configuration.GetDataProviderName());
            Assert.AreEqual("Test logging provider"configuration.GetLoggingProviderName());
            Assert.AreEqual("Test logging provider"configuration.GetDataProviderLoggingProviderName());
        }
    }
}

Build the project. When you run the tests, they all pass. This demonstrates that the test versions of the dependencies have been injected.



Friday, September 25, 2020

gRPC and .Net Core

 When Microsoft started pushing .Net Core they announced they would not be supporting WCF. This caused quite a stir in the Microsoft community so Microsoft relented and added WCF support that became available with the Visual Studio 2017 version 15.5 release. 

Nevertheless, Microsoft is still touting the advantages of gRPC rather than WCF for new .Net Core development. Reading through the gRPC documentation I am unhappy at the poor support for Guid, Decimal, DateTime, and TimeSpan types. These are all types we use a lot. If Google can have their own custom Protobuf types, why can't Microsoft?

Visual Studio has a project template of gRPC Service which, like the WCF Service Application project template, contains some example code. Let's see what's in here, and how we would write a client to consume their example service. I'm using Visual Studio 2019 Enterprise version 16.7.4.

Start a new project using the gRPC Service template. Call it HelloService.

Take all the defaults. Once the project has been created you will see a HelloService tab. Close it. You can build and run the service immediately. It is self hosting. There is a console program in program.cs that configures it and launches it. There are other hosting options that I'm not going to get into in this blog entry. You can read the Microsoft introduction to gRPC for WCF developers to understand how it works. Pay special attention to the proto file which replaces the interface that WCF users.

If you run the service now (which you need to) you will see a console like this. The service must be running for the client to work.



To write the host, start a new copy of Visual Studio and create a new C# console app called HelloClient.


Accessing the gRPC service has the same types of steps as accessing a WCF client. We start by creating a reference to it in the project, then in code we create a channel and execute a method.

Creating the reference.

Right-click on dependencies and select Add Connected Service. This displays a page that allows us to add a Service Reference. Click the Add button in the Service References section.



Now select gRPC and click [Next]. I find it easiest to browse to the HelloService's proto file so click on [Browse] and browse to the proto file.


Back at the 'Add new gRPC service reference" window, click [Finish], then click [Close]. The HelloClient tab should now look like this.


Code Time!

Start by adding a using for Grpc.Net.Client and HelloService. Then add code to create a channel and an instance of the Greeter service. Program.cs looks like this.

using System;
using Grpc.Net.Client;
using HelloService;
 
namespace HelloClient
{
    class Program
    {
        static void Main(string[] args)
        {
            GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001");
            Greeter.GreeterClient client = new Greeter.GreeterClient(channel);
        }
    }
}

Now we create and populate the request and the reply. Append this code.

            HelloRequest request = new HelloRequest() { Name = "Terry" };
            HelloReply reply = client.SayHello(request);

Lastly we pull the data out of the reply.

            Console.WriteLine(reply.Message);

At this point Program.cs should look like this.

using System;
using Grpc.Net.Client;
using HelloService;
 
namespace HelloClient
{
    class Program
    {
        static void Main(string[] args)
        {
            GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001");
            Greeter.GreeterClient client = new Greeter.GreeterClient(channel);
            HelloRequest request = new HelloRequest() { Name = "Terry" };
            HelloReply reply = client.SayHello(request);
            Console.WriteLine(reply.Message);
        }
    }
}

When you run HelloClient you will see a console window like this.


As you can see, the consumption of the gRPC service is quite similar to the consumption of a WCF service.

PS. A few days after I wrote this blog entry I found this tutorial on microsoft.com which is almost identical.