The problem

During the implementation of a feature in my work, I came across a problem:

  • I had multiple implementations of one service interface, and I needed to inject one of them based on the data type it should process.
  • These implementations had other dependencies, so I needed a factory that would build a whole dependency tree for me (using .NET dependency injection).
  • The factory also needed to be easy to extend, without having to change the code.

With these criteria in mind, I searched the internet for inspiration. Unfortunately, I could not find anything helpful. So, I decided to solve the problems myself, and share the idea with you.

For the sake of demonstration, I have created an example project you can find on GitHub. In this article. I will guide you through the code and explain the idea so you can apply this in your own project.

The function

The example here is the HTTP-triggered AreaCalculator function, which can compute the area of common geometrical shapes. It is triggered via the POST method with the route calculateArea/{shape}. The {shape} route parameter represents the shape for which the area will be calculated. It is specified as JSON in the body of the request.

As you can see, we are injecting the IAreaCalculatorFactory factory here. The factory provides the required implementation of the calculator. Implementation is provided based on the shape type parsed from the route parameter. The calculator then computes the area of the specific shape. The result is then returned as a JSON in the OK response message.

public class AreaCalculator
    {
        private readonly IAreaCalculatorFactory _calculatorFactory;
        private readonly ILogger<AreaCalculator> _logger;

        public AreaCalculator(IAreaCalculatorFactory calculatorFactory, ILogger<AreaCalculator> log)
        {
            _calculatorFactory = calculatorFactory;
            _logger = log;
        }

        [FunctionName(nameof(AreaCalculator))]
        [OpenApiOperation(operationId: "Run", tags: new[] { "name" })]
        [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
        [OpenApiParameter(name: "shape", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "The shape (circle, square, rectangle, or thriangle)")]
        [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(AreaResult), Description = "The area of the shape specified, rounded to 2 decimal places")]
        public async Task<IActionResult> RunAsync(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = "calculateArea/{shape}")] HttpRequest req,
            string shape,
            CancellationToken ct)
        {
            _logger.LogInformation("Executing area calculator for shape: {shape}", shape);

            if (!Enum.TryParse(shape, true, out Shape shapeType))
                return new BadRequestObjectResult($"{shape} shape not supported");

            var calculator = _calculatorFactory.GetAreaCalculatorForShape(shapeType);

            var result = await calculator.CalculateAreaAsync(req.Body, ct);

            return new OkObjectResult(new AreaResult(result));
        }
    }

The service factory

The area calculator factory is straightforward. It holds a dictionary of registered calculator services for each available shape type and implements the GetAreaCalculatorForShape method. This method retrieves registered calculator services directly from the IServiceProvider based on the shape type provided as a parameter.

Public class AreaCalculatorFactory : IareaCalculatorFactory
    {
        private Dictionary<Shape, Type> _registeredCalculators;

        private readonly IserviceProvider _serviceProvider;

        public AreaCalculatorFactory(IserviceProvider serviceProvider, Dictionary<Shape, Type> registeredCalculators)
        {
            _registeredCalculators = registeredCalculators;
            _serviceProvider = serviceProvider;
        }

        public IareaCalculatorService GetAreaCalculatorForShape(Shape shape)
        {
            if (!_registeredCalculators.TryGetValue(shape, out var calculatorType) || calculatorType == null)
                throw new InvalidOperationException($”No calculator registered for shape {shape}");

            return (IareaCalculatorService)_serviceProvider.GetRequiredService(calculatorType);
        }
    }

The service factory builder

Now you might wonder how this can be easily initialized and registered, so it can be later injected where needed. The service provider that is injected in the factory is immutable and must already contain all calculator services and their dependency tree. Where and how do you register calculators then? The answer is AreaCalculatorFactoryBuilder. It takes service collection in its constructor, which means that inside, you can register specific calculator services for specific shape types and save the Shape → IAreaCalculatorService mapping at once (RegisterCalculatorForType method).

public class AreaCalculatorFactoryBuilder
    {
        private Dictionary<Shape, Type> _registeredCalculators;
        private readonly IServiceCollection _services;

        public AreaCalculatorFactoryBuilder(IServiceCollection services)
        {
            _registeredCalculators = new Dictionary<Shape, Type>();
            _services = services;
        }

        public AreaCalculatorFactoryBuilder RegisterCalculatorForType<TCalculator>(Shape shape)
            where TCalculator : IAreaCalculatorService
        {
            _services.AddScoped(typeof(TCalculator));
            _registeredCalculators.Add(shape, typeof(TCalculator));

            return this;
        }

        public IAreaCalculatorFactory Build(IServiceProvider serviceProvider)
        {
            return new AreaCalculatorFactory(serviceProvider, _registeredCalculators);
        }
    }

Service registration

The factory and its services are then registered as shown below. The AreaCalculatorFactoryBuilder is initialized beforehand, and the Build method is called inside the service provider delegate when the ICalculatorFactory implementation is needed. Here you can also see the registration of the custom JSON parser which is used within the calculator services to deserialize the request body stream into data needed for area calculation. It demonstrates that the services provided by the factory can have other dependencies injected using .NET dependency injection.

public static class ServiceCollectionExtensions
    {
        public static IServiceCollection AddServices(this IServiceCollection services)
        {
            // Parsers
            services.AddScoped<ICustomParser, ShapeParser>();

            // Area calculators
            var areaCalculatorFactoryBuilder = new AreaCalculatorFactoryBuilder(services)
                .RegisterCalculatorForType<TriangleAreaCalculator>(Shape.Triangle)
                .RegisterCalculatorForType<SquareAreaCalculator>(Shape.Square)
                .RegisterCalculatorForType<RectangleAreaCalculator>(Shape.Rectangle)
                .RegisterCalculatorForType<CircleAreaCalculator>(Shape.Circle);
            services.AddScoped(x => areaCalculatorFactoryBuilder.Build(x));
            return services;
        }
    }

Explanation of the remaining code

Lastly, I will shortly explain a code snippet of the CircleAreaCalculator and the ShapeParser, just to provide all implementation details. As you can see, CircleAreaCalculator is an implementation of the abstract class AreaCalculatorBase. AreaCalculatorBase handles the parsing and provides data to the specific implementation to do specific calculations. The parser uses a basic .NET JSON serializer with custom options to deserialize the stream into the required type.

public class CircleAreaCalculator : AreaCalculatorBase<Circle>
{
    public CircleAreaCalculator(ICustomParser shapeParser) : base(shapeParser) { }

    protected override double CalculateArea(Circle circle)
        => Math.Pow(circle.R, 2) * Math.PI;
}

public abstract class AreaCalculatorBase<TShape> : IAreaCalculatorService
{
    private readonly ICustomParser _shapeParser;

    public AreaCalculatorBase(ICustomParser shapeParser)
    {
        _shapeParser = shapeParser;
    }

    private async Task<TShape> GetShapeAsync(Stream stream, CancellationToken ct) 
        => await _shapeParser.ParseJsonBodyAsync<TShape>(stream, ct);

    public async Task<double> CalculateAreaAsync(Stream stream, CancellationToken ct)
        => CalculateArea(await GetShapeAsync(stream, ct));

    protected abstract double CalculateArea(TShape shape);
}

public class ShapeParser : ICustomParser
{
    private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    public async Task<T> ParseJsonBodyAsync<T>(Stream stream, CancellationToken ct = default)
    {
        T? val = await JsonSerializer.DeserializeAsync<T>(stream, SerializerOptions, ct);
        if (val == null)
        {
            throw new JsonException("Null payload");
        }

        return val;
    }
}

Summary

The resulting solution is:

  • Easy to use → it uses build-in .NET dependency injection.
  • Open for extension, closed for modification → we can easily add support for other shapes without a need to change the code.
  • Simple to implement → can be done in three simple steps:
    1. Implement service factory using the IServiceProvider inside (it is the key to utilizing .NET dependency injection).
    2. Implement a service factory builder, that will simplify service registration and factory builder creation.
    3. Provide a service factory delegate, that will build the required service factory when needed.

The example project can be run locally. Just run the function app and open the following address in your browser: http://localhost:{function-app-port}/api/swagger/ui. Here you can use the Swagger UI to make calls to API.



Author
Adrian Hrinko
Full-stack developer @Safetica

Next articles

Do you know std::string_view and how to use it?

Read an article written by one of our developers explaining the std::string_view part of the code.

Microsoft Identity Platform: Introduction

Do you need a reliable identity management service for your application? Don't reinvent the wheel by coding your own. Try Microsoft Identity Platform. This article will introduce you to the service and give you information on how to start adopting it.

Signing drivers through Windows HLK

Let’s go together over the steps that are required in order to sign a driver using Microsoft HLK.