Při implementaci jedné funkce v rámci své práce jsem narazil na následující problém:
S těmito kritérii v hlavě jsem hledal inspiraci na internetu. Bohužel jsem nenašel nic užitečného. Rozhodl jsem se tedy vyřešit problém sám a podělit se s vámi o nápad.
Pro účely demonstrace jsem vytvořil ukázkový projekt, který najdete na GitHubu. V tomto článku vás provedu kódem a vysvětlím myšlenku, abyste ji mohli aplikovat ve svém vlastním projektu.
Příkladem je zde funkce AreaCalculator spouštěná přes HTTP, která dokáže vypočítat obsah běžných geometrických tvarů. Spouští se metodou POST s cestou calculateArea/{shape}. Parametr cesty {shape} reprezentuje tvar, pro který bude obsah vypočítán. Data tvaru jsou specifikována jako JSON v těle požadavku.
Jak vidíte, injektujeme zde IAreaCalculatorFactory. Factory poskytuje požadovanou implementaci kalkulátoru na základě typu tvaru parsovaného z parametru cesty. Kalkulátor poté vypočítá obsah konkrétního tvaru a výsledek je vrácen jako JSON v odpovědi 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));
}
}
Factory pro výpočet obsahu je přímočará. Drží slovník registrovaných služeb kalkulátoru pro každý dostupný typ tvaru a implementuje metodu GetAreaCalculatorForShape. Tato metoda získává registrované služby kalkulátoru přímo z IServiceProvider na základě poskytnutého typu tvaru.
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);
}
}
Nyní vás možná zajímá, jak lze toto všechno snadno inicializovat a zaregistrovat tak, aby to mohlo být později injektováno tam, kde je to potřeba. Service provider injektovaný do factory je neměnný a musí již obsahovat všechny služby kalkulátoru a jejich strom závislostí.
Kde a jak tedy zaregistrovat kalkulátory? Odpověď je AreaCalculatorFactoryBuilder. Bere ve svém konstruktoru kolekci služeb, což znamená, že můžete zaregistrovat konkrétní služby kalkulátoru pro konkrétní typy tvarů a zároveň pomocí metody RegisterCalculatorForType uložit mapování Shape → IAreaCalculatorService.
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);
}
}
Factory a její služby jsou poté registrovány tak, jak je uvedeno níže. AreaCalculatorFactoryBuilder je inicializován předem a metoda Build je volána uvnitř delegátu service provideru, když je potřeba implementace IAreaCalculatorFactory.
Vidíte zde také registraci vlastního JSON parseru používaného v rámci služeb kalkulátoru k deserializaci streamu těla požadavku do dat potřebných pro výpočet obsahu. To ukazuje, že služby poskytované factory mohou mít další závislosti injektované pomocí .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;
}
}
Nakonec zde je krátké vysvětlení implementací CircleAreaCalculator a ShapeParser. Jak vidíte, CircleAreaCalculator je implementací abstraktní třídy AreaCalculatorBase. AreaCalculatorBase se stará o parsování a poskytuje data konkrétním implementacím, aby mohly provádět požadované výpočty. Parser používá vestavěný .NET JSON serializer s vlastními možnostmi pro deserializaci streamu do požadovaného typu.
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;
}
}
Výsledné řešení je:
Ukázkový projekt lze spustit lokálně. Stačí spustit function app a otevřít v prohlížeči http://localhost:{function-app-port}/api/swagger/ui. Tam můžete použít Swagger UI k provádění API volání.