Binding a Web Job to Azure BLOB Storage Containers defined in Configuration
I have a Web Job that needs to response to BLOBs being created in multiple Azure Storage Containers. This would be straight forward except that I also need the containers to be defined in Configuration so that I can add a container without doing a deployment. I started reflecting into how AddAzureStorage() does this but it quickly became more complex than I wanted to tackle. At this point I came up with the idea of using Roslyn to compile an assembly at runtime and add it to my app domain before invoking AddAzureStorage()
- Create a wrapper around Roslyn
I ended up making a Fluent API wrapper around Roslyn to make it easier to invoke from my Program.cs
public class Compiler
{
private readonly string _assmeblyName;
private readonly List<string> _syntaxTrees = new List<string>();
private readonly List<Assembly> _references = new List<Assembly>()
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly,
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
};public Compiler(string assmeblyName)
{
if (string.IsNullOrWhiteSpace(assmeblyName)) throw new ArgumentException($"'{nameof(assmeblyName)}' cannot be null or whitespace", nameof(assmeblyName));this._assmeblyName = assmeblyName;
}public Compiler WithReference(Assembly reference)
{
this._references.Add(reference ?? throw new ArgumentNullException(nameof(reference)));return this;
}public Compiler WithSyntaxTree(string syntaxTree)
{
if (string.IsNullOrWhiteSpace(syntaxTree)) throw new ArgumentException($"'{nameof(syntaxTree)}' cannot be null or whitespace", nameof(syntaxTree));this._syntaxTrees.Add(syntaxTree);return this;
}public Assembly Compile()
{
CSharpParseOptions options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest);
CSharpCompilation csharpCompilation = CSharpCompilation.Create(
$"{this._assmeblyName}.dll",
this._syntaxTrees.Select(_ => SyntaxFactory.ParseSyntaxTree(_, options)).ToArray(),
references: this._references.Distinct().Select(_ => MetadataReference.CreateFromFile(_.Location)).ToArray(),
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));
using (MemoryStream strm = new MemoryStream())
{
EmitResult result = csharpCompilation.Emit(strm);if (!result.Success)
{
throw new AggregateException(
"Compilation completed with errors",
result.Diagnostics
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
.Select(_ => new Exception(_.ToString()))
.ToArray());
}
else
{
return AppDomain.CurrentDomain.Load(strm.ToArray());
}
}
}
}
2. Create a Code File and Invoke Compiler
Next, I created a code file representing the class with the Azure Storage bindings. I have it inheriting from an abstract base class in the Web Job project so that I can have most of my code verified at compile time instead of run time.
private static void CreateRuntimeFunctions()
{
string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();configurationBuilder.AddJsonFile("appsettings.json", false);
configurationBuilder.AddJsonFile($"appsettings.{environment}.json", true);
configurationBuilder.AddEnvironmentVariables();
configurationBuilder.AddAzureKeyVaultFromConfig();IConfigurationRoot configurationRoot = configurationBuilder.Build();
ConfigurationOptions configurationOptions = configurationRoot.GetSection("MyConfiguration").Get<ConfigurationOptions>();string assemblyName = typeof(Program).Assembly.GetName().Name + ".Runtime";
new Compiler(assemblyName)
.WithReference(System.Reflection.Assembly.Load("netstandard"))
.WithReference(typeof(System.IServiceProvider).Assembly)
.WithReference(typeof(System.IO.Stream).Assembly)
.WithReference(typeof(System.Threading.Tasks.Task).Assembly)
.WithReference(typeof(Microsoft.Azure.WebJobs.BlobTriggerAttribute).Assembly)
.WithReference(typeof(Microsoft.Extensions.Logging.ILogger<>).Assembly)
.WithReference(typeof(Program).Assembly)
.WithSyntaxTree($@"
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;namespace {assemblyName}
{{
public class DynamicFunctions : Functions
{{
{string.Join(
Environment.NewLine,
configurationOptions.Containers.Select(container => $@"
public async Task Process{container.Key}BlobAsync(
[BlobTrigger(""{container.Value.Container}/{container.Key.ToLowerInvariant()}/{{blobName}}"", Connection = ""MyBlobConnection"")] Stream blobContents,
string blobName,
CancellationToken cancellationToken = default) => await base.ProcessBlobAsync(""{container.Key}"", blobName, blobContents, cancellationToken);
").ToArray())}
}}
}}
")
.Compile();
}
}
}
3. Bringing it all together
The last step is invoking CreateRuntimeFunctions() before AddAzureStorage()
public static async Task Main(string[] args)
{
try
{
CreateRuntimeFunctions();HostBuilder builder = new HostBuilder();
Startup startup = null;builder.ConfigureAppConfiguration(c =>
{
string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
c.AddJsonFile("appsettings.json", false);
c.AddJsonFile($"appsettings.{environment}.json", true);
c.AddEnvironmentVariables();
});builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
});