Adapter pattern in plug-and-play architecture in Asp.NET 5

After the last blog post we are prepared to start the implementation of architecture aspects.

First, we need to create a new project where we will place the aspects. We don't want to place them in an executable project because we will be using them in both web and console applications in the next blog post. The new project is named "PlugAndPlayExample.Infrastructure".

image.png

For simplicity's sake, we won't implement all infrastructure aspects that are almost always required for a reliable architecture, and maybe we will cover them in another series. Right now it is enough to implement a caching mechanism and a logger. After we are done the project and file structure should look like this:

image.png

Both aspect implementations will follow the adapter pattern for their respective functionalities, and that will allow us to swap them as needed, either by configuration or by convention.

The adapter pattern dictates that we create an interface that the implementations will follow:

using System;

namespace PlugAndPlayExample.Infrastructure.Logging
{
    public interface ILogger
    {
        void Trace(string message);

        void Info(string message);

        void Warn(string message);

        void Error(string message);

        void Fatal(string message);

        void Info(string message, Exception exception);

        void Warn(string message, Exception exception);

        void Error(string message, Exception exception);

        void Fatal(string message, Exception exception);
    }
}

And then create all implementations that we want or need. For examples sake we will create simple console and file loggers:

using System;

namespace PlugAndPlayExample.Infrastructure.Logging
{
    public class ConsoleLogger : ILogger
    {
        public void Error(string message)
        {
            Console.Error.WriteLine("ERROR: {0}", message);
        }

        public void Error(string message, Exception exception)
        {
            Console.Error.WriteLine("ERROR: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Fatal(string message)
        {
            Console.Error.WriteLine("FATAL: {0}", message);
        }

        public void Fatal(string message, Exception exception)
        {
            Console.Error.WriteLine("FATAL: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Info(string message)
        {
            Console.Error.WriteLine("INFO: {0}", message);
        }

        public void Info(string message, Exception exception)
        {
            Console.Error.WriteLine("INFO: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Trace(string message)
        {
            Console.Error.WriteLine("TRACE: {0}", message);
        }

        public void Warn(string message)
        {
            Console.Error.WriteLine("WARN: {0}", message);
        }

        public void Warn(string message, Exception exception)
        {
            Console.Error.WriteLine("WARN: {0}{1}{2}", message, Environment.NewLine, exception);
        }
    }
}
using System;
using System.IO;

namespace PlugAndPlayExample.Infrastructure.Logging
{
    public class FileLogger : ILogger
    {
        private readonly FileInfo file;

        public FileLogger(FileInfo file)
        {
            this.file = file;
            if (!file.Exists)
            {
                file.Create();
            }
        }
        public void Error(string message)
        {
            WriteLine("ERROR: {0}", message);
        }

        public void Error(string message, Exception exception)
        {
            WriteLine("ERROR: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Fatal(string message)
        {
            WriteLine("FATAL: {0}", message);
        }

        public void Fatal(string message, Exception exception)
        {
            WriteLine("FATAL: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Info(string message)
        {
            WriteLine("INFO: {0}", message);
        }

        public void Info(string message, Exception exception)
        {
            WriteLine("INFO: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        public void Trace(string message)
        {
            WriteLine("TRACE: {0}", message);
        }

        public void Warn(string message)
        {
            WriteLine("WARN: {0}", message);
        }

        public void Warn(string message, Exception exception)
        {
            WriteLine("WARN: {0}{1}{2}", message, Environment.NewLine, exception);
        }

        private void WriteLine(string format, params object[] args)
        {
            using(var writer = new StreamWriter(file.OpenWrite()))
            {
                writer.WriteLine(format, args);
            }
        }
    }
}

We need to do the same thing for the caching aspect, but for now we will only implement an in-memory one and not worry about other implementations.

namespace PlugAndPlayExample.Infrastructure.Caching
{
    public interface ICache
    {
        T Get<T>(string key) where T : class;

        void Set<T>(string key, T value);
    }
}
using System.Collections.Generic;

namespace PlugAndPlayExample.Infrastructure.Caching
{
    public class InMemoryCache : ICache
    {
        private readonly Dictionary<string, object> _cache = new Dictionary<string, object>();

        public T Get<T>(string key) where T : class
        {
            if(!string.IsNullOrEmpty(key) && _cache.ContainsKey(key))
                return _cache[key] as T;

            return default(T);
        }

        public void Set<T>(string key, T value)
        {
            _cache.Add(key, value);
        }

        public object this[string key]
        {
            get { return Get<object>(key); }
            set { Set(key, value); }
        }
    }
}

Latter on if we want to use some other caching mechanism, like Redis, we can just create another implementation that will utilize the Redis caching server and swap the registered implementation in the service container.

In order to register the newly created aspects we will create another extension method:

using Microsoft.Extensions.DependencyInjection;
using PlugAndPlayExample.Infrastructure.Caching;
using PlugAndPlayExample.Infrastructure.Logging;
using System.IO;

namespace PlugAndPlayExample.Configuration
{
    public static class RegisterInfrastructureAspectsExtension
    {
        public static IServiceCollection RegisterInfrastructureAspects(this IServiceCollection services)
        {
            services.AddSingleton<ICache, InMemoryCache>();
            services.AddSingleton<ILogger, FileLogger>(provider => new FileLogger(new FileInfo("my_log_file.log")));

            return services;
        }
    }
}

And call that extension method in the startup class

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.RegisterServices();
            services.RegisterInfrastructureAspects();
        }

This is a clear example of how the Adapter pattern and the "Dependency Injection" "Inversion of Control" pattern play together to allow us a simple decoupling of concerns and allow us to swap real implementations. Lets see how to do that in a command:

using PlugAndPlayExample.Infrastructure.Caching;
using PlugAndPlayExample.Services.Infrastructure;

namespace PlugAndPlayExample.Services.Commands
{
    public class AddUser
    {
        public class Request: ICommand
        {
            public string UserName { get; set; }
            public string Password { get; set; }
        }

        public class Handler : ICommandHandler<Request, Response<int>>
        {
            private readonly ICache cache;

            public Handler(ICache cache)
            {
                this.cache = cache;
            }

            public Response<int> Handle(Request command)
            {
                var user = // User registration code...

                cache.Set<User>($"user-{user.Id}", user);

                return user.Id;
            }
        }
    }
}

In this example, the DI (Dependency Injection) container will inject the InMemoryCache implementation for our ICache service. If we want to use Redis we can create another implementation of the ICache interface that will use the Redis server, register it in the service container as an implementation of the ICache interface, and the DI container will inject it in our command handler.

In the next blog post we will see how we can create an execution pipeline and inject some aspects in it, like the logger, and manipulate that it to fit our needs as part of a convention or through a configuration.

Happy coding,
DotNetGuru

Sort:  

Your content has been voted as a part of Encouragement program. Keep up the good work!

Use Ecency daily to boost your growth on platform!

Support Ecency
Vote for new Proposal
Delegate HP and earn more

Congratulations @dotnetguru! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):

You received more than 50 upvotes.
Your next target is to reach 100 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Check out the last post from @hivebuzz:

Hive Power Up Month - Feedback from Day 7
Happy New Year - Feedback from the first Hive Power Up Day of 2022
Support the HiveBuzz project. Vote for our proposal!