Durante a implementação de uma funcionalidade no meu trabalho, deparei-me com um problema:
Com estes critérios em mente, procurei inspiração na internet. Infelizmente, não consegui encontrar nada útil. Por isso, decidi resolver o problema sozinho e partilhar a ideia consigo.
Para efeitos de demonstração, criei um projeto exemplo que pode encontrar no GitHub. Neste artigo, vou guiá-lo através do código e explicar a ideia, para que a possa aplicar no seu próprio projeto.
O exemplo aqui é a função AreaCalculator, despoletada por HTTP, que consegue calcular a área de figuras geométricas comuns. É despoletada via método POST com a rota calculateArea/{shape}. O parâmetro de rota {shape} representa a figura para a qual a área será calculada. Os dados da figura são especificados como JSON no corpo do pedido.
Como pode ver, estamos a injetar aqui o IAreaCalculatorFactory. A factory disponibiliza a implementação necessária da calculadora com base no tipo de figura analisado a partir do parâmetro de rota. A calculadora calcula então a área da figura específica e o resultado é devolvido como JSON na resposta OK.
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 triangle)"
)]
[OpenApiResponseWithBody(
statusCode: HttpStatusCode.OK,
contentType: "application/json",
bodyType: typeof(AreaResult),
Description = "The area of the specified shape, 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));
}
}
A area calculator factory é simples. Mantém um dicionário de serviços de calculadora registados para cada tipo de figura disponível e implementa o método GetAreaCalculatorForShape. Este método obtém os serviços de calculadora registados diretamente do IServiceProvider com base no tipo de figura indicado.
public class AreaCalculatorFactory : IAreaCalculatorFactory
{
private readonly 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);
}
}
Agora pode estar a perguntar-se como é que isto pode ser facilmente inicializado e registado para que possa ser injetado mais tarde onde for necessário. O service provider injetado na factory é imutável e tem de conter já todos os serviços de calculadora e a sua árvore de dependências.
Onde e como é que se registam então as calculadoras? A resposta é o AreaCalculatorFactoryBuilder. Recebe a service collection no seu construtor, o que significa que pode registar serviços de calculadora específicos para tipos de figura específicos e, em simultâneo, guardar o mapeamento Shape → IAreaCalculatorService usando o método RegisterCalculatorForType.
public class AreaCalculatorFactoryBuilder
{
private readonly 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);
}
}
A factory e os seus serviços são depois registados como mostrado abaixo. O AreaCalculatorFactoryBuilder é inicializado previamente e o método Build é invocado dentro do delegate do service provider quando a implementação de IAreaCalculatorFactory for necessária.
Pode também ver o registo do parser de JSON personalizado utilizado nos serviços de calculadora para deserializar o stream do corpo do pedido nos dados necessários ao cálculo da área. Isto demonstra que os serviços disponibilizados pela factory podem ter outras dependências injetadas através da dependency injection do .NET.
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;
}
}
Por último, eis uma breve explicação das implementações CircleAreaCalculator e ShapeParser. Como pode ver, CircleAreaCalculator é uma implementação da classe abstrata AreaCalculatorBase. AreaCalculatorBase trata da análise e fornece dados às implementações específicas para que possam efetuar os cálculos necessários. O parser usa o serializador de JSON incorporado no .NET, com opções personalizadas, para deserializar o stream para o tipo pretendido.
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;
}
}
A solução resultante é:
O projeto exemplo pode ser executado localmente. Basta executar a função app e abrir http://localhost:{function-app-port}/api/swagger/ui no seu navegador. Aí pode usar o Swagger UI para fazer chamadas de API.