如何让C#编写的Azure Function App更符合DRY原则?
解决方案
一、用元编程(Source Generator)自动生成重复类
如果想保留编译时的Function注册逻辑,可以通过C# Source Generator自动生成多个重复的Function类,避免手动编写冗余代码:
- 定义标记属性(用于指定要生成的配置名称):
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public class GenerateTaskEventGeneratorAttribute : Attribute { public string ConfigName { get; } public GenerateTaskEventGeneratorAttribute(string configName) { ConfigName = configName; } }
- 实现Source Generator,扫描配置名称并生成对应类:
[Generator] public class TaskEventGeneratorSourceGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { // 这里可以从常量类或配置文件读取所有需要生成的配置名称 var configNames = new[] { "InitialUpload", "DailySync", "HourlyScan" }; foreach (var configName in configNames) { var className = $"{configName}NewOrModifiedFileTaskEventGenerator"; var functionName = $"GenerateFor{configName}"; var sourceCode = $$""" using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.WebJobs; using YourProjectNamespace; public class {{className}} { private readonly TaskEventGeneratorConfig _taskEventGeneratorConfig; private readonly NewOrModifiedFileTaskEventGeneratorHelper _taskEventGeneratorHelper; private const string _configName = "{{configName}}"; public {{className}}( ITaskEventGeneratorConfigFactory taskEventGeneratorConfigFactory, NewOrModifiedFileTaskEventGeneratorHelper taskEventGeneratorHelper ) { _taskEventGeneratorConfig = Guard.Against.Null(taskEventGeneratorConfigFactory) .GetConfig(_configName); _taskEventGeneratorHelper = Guard.Against.Null(taskEventGeneratorHelper); } [FunctionName("{{functionName}}")] [SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Used for Azure Function")] public async Task Generate( [TimerTrigger($"%{{_configName}}:DirectoryScanFrequencyCron%")] TimerInfo timer, CancellationToken cancellationToken ) => await _taskEventGeneratorHelper.Generate(_taskEventGeneratorConfig, cancellationToken); } """; context.AddSource($"{className}.g.cs", sourceCode); } } }
编译时会自动生成所有对应配置名称的Function类,无需手动编写重复代码。
二、进阶:动态注册定时器函数,彻底摆脱硬编码
如果想完全通过配置驱动,不用硬编码任何配置名称,可以借助Azure Functions的动态作业注册功能:
- 修改appsettings.json为数组配置:
{ "TaskEventGenerators": [ { "ConfigName": "InitialUpload", "DirectoryScanFrequencyCron": "0 */5 * * * *", // 其他配置字段 }, { "ConfigName": "DailySync", "DirectoryScanFrequencyCron": "0 0 0 * * *", // 其他配置字段 } ] }
- 创建配置模型:
public class TaskEventGeneratorConfigItem { public string ConfigName { get; set; } public string DirectoryScanFrequencyCron { get; set; } // 补充其他需要的配置属性 }
- 在Program.cs中动态注册定时器:
var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureServices((context, services) => { // 绑定配置数组 var configItems = context.Configuration.GetSection("TaskEventGenerators") .Get<List<TaskEventGeneratorConfigItem>>(); services.AddSingleton(configItems); services.AddSingleton<NewOrModifiedFileTaskEventGeneratorHelper>(); services.AddSingleton<ITaskEventGeneratorConfigFactory, TaskEventGeneratorConfigFactory>(); }) .Build(); // 遍历配置,动态注册每个定时器函数 var configItems = host.Services.GetRequiredService<List<TaskEventGeneratorConfigItem>>(); var helper = host.Services.GetRequiredService<NewOrModifiedFileTaskEventGeneratorHelper>(); var configFactory = host.Services.GetRequiredService<ITaskEventGeneratorConfigFactory>(); foreach (var item in configItems) { var config = configFactory.GetConfig(item.ConfigName); host.RegisterTimerFunction( functionName: $"GenerateFor{item.ConfigName}", cronExpression: item.DirectoryScanFrequencyCron, executeAsync: async (timerInfo, cancellationToken) => { await helper.Generate(config, cancellationToken); } ); } await host.RunAsync();
注:RegisterTimerFunction是Azure Functions Worker SDK的扩展方法,需确保使用最新版本的Microsoft.Azure.Functions.Worker包。
这种方式下,新增配置只需修改appsettings.json,无需改动任何代码,完全实现配置驱动。
两种方案对比
- Source Generator方案:保留编译时Function注册,适合需要静态检查Function存在性的场景;
- 动态注册方案:灵活性更高,适合配置频繁变动的场景,彻底消除硬编码。
内容的提问来源于stack exchange,提问作者Brian Kessler
相关产品推荐
相关产品推荐

