<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-AU" xmlns:media="http://search.yahoo.com/mrss/">
  <id>https://david.gardiner.net.au/tags/Azure%20Functions.xml</id>
  <title type="html">David Gardiner - Azure Functions</title>
  <updated>2026-05-24T00:34:41.762Z</updated>
  <subtitle>Blog posts tagged with &apos;Azure Functions&apos; - A blog of software development, .NET and other interesting things</subtitle>
  <rights>Copyright 2026 David Gardiner</rights>
  <icon>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=64</icon>
  <logo>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=256</logo>
  <generator uri="https://github.com/flcdrg/astrojs-atom" version="3">astrojs-atom</generator>
  <author>
    <name>David Gardiner</name>
  </author>
  <link href="https://david.gardiner.net.au/tags/Azure%20Functions.xml" rel="self" type="application/atom+xml"/>
  <link href="https://david.gardiner.net.au/tags/Azure%20Functions" rel="alternate" type="text/html" hreflang="en-AU"/>
  <category term="Azure Functions"/>
  <category term="Software Development"/>
  <entry>
    <id>https://david.gardiner.net.au/2025/04/azure-function-isolated</id>
    <updated>2025-04-01T21:30:00.000+10:30</updated>
    <title>ConfigureFunctionsWorkerDefaults vs ConfigureFunctionsWebApplication in .NET Azure Functions</title>
    <link href="https://david.gardiner.net.au/2025/04/azure-function-isolated" rel="alternate" type="text/html" title="ConfigureFunctionsWorkerDefaults vs ConfigureFunctionsWebApplication in .NET Azure Functions"/>
    <category term=".NET"/>
    <category term="Azure Functions"/>
    <published>2025-04-01T21:30:00.000+10:30</published>
    <summary type="html">When to use ConfigureFunctionsWorkerDefaults versus ConfigureFunctionsWebApplication in .NET isolated Azure Functions.</summary>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX_2tR59E.webp&quot; alt=&quot;Azure Functions logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When you&apos;re creating a .NET Azure Function using the isolated worker model, the &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide?tabs=hostbuilder%2Cwindows&amp;amp;WT.mc_id=DOP-MVP-5001655#configuration&quot;&gt;default template creates code&lt;/a&gt; that has the host builder configuration calling &lt;code&gt;ConfigureFunctionsWorkerDefaults&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But if you want to &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide?tabs=hostbuilder%2Cwindows&amp;amp;WT.mc_id=DOP-MVP-5001655#aspnet-core-integration&quot;&gt;enable ASP.NET Core integration&lt;/a&gt; features (which you would do if you wanted to work directly with &lt;code&gt;HttpRequest&lt;/code&gt;, &lt;code&gt;HttpResponse&lt;/code&gt;, and &lt;code&gt;IActionResult&lt;/code&gt; types in your functions), then the documentation &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide?tabs=hostbuilder%2Cwindows&amp;amp;WT.mc_id=DOP-MVP-5001655#aspnet-core-integration&quot;&gt;suggests to instead call ConfigureFunctionsWebApplication&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Usefully, once you add a package reference to &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore/&quot;&gt;Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore&lt;/a&gt;, you&apos;ll also get a .NET code analyzer error &lt;a href=&quot;https://dotnet-worker-rules.azurewebsites.net/rules?ruleid=AZFW0014&quot;&gt;AZFW0014&lt;/a&gt; if you&apos;re not calling &lt;code&gt;ConfigureFunctionsWebApplication&lt;/code&gt;, so the compiler will ensure you&apos;re doing the right thing.&lt;/p&gt;
&lt;p&gt;But apart from the integration with ASP.NET Core, are there any other significant differences between calling the two methods?&lt;/p&gt;
&lt;p&gt;Because the code is hosted on GitHub, we can easily review the source code to find out.&lt;/p&gt;
&lt;h2&gt;ConfigureFunctionsWorkerDefaults&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ConfigureFunctionsWorkerDefaults&lt;/code&gt; is defined in the &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker/blob/main/src/DotNetWorker/Hosting/WorkerHostBuilderExtensions.cs&quot;&gt;WorkerHostBuilderExtensions&lt;/a&gt; class in the &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker&quot;&gt;https://github.com/Azure/azure-functions-dotnet-worker&lt;/a&gt; project. There are a number of overloads, but they all end up calling this implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static IHostBuilder ConfigureFunctionsWorkerDefaults(this IHostBuilder builder, Action&amp;lt;HostBuilderContext, IFunctionsWorkerApplicationBuilder&amp;gt; configure, Action&amp;lt;WorkerOptions&amp;gt;? configureOptions)
{
    builder
        .ConfigureHostConfiguration(config =&amp;gt;
        {
            // Add AZURE_FUNCTIONS_ prefixed environment variables
            config.AddEnvironmentVariables(&quot;AZURE_FUNCTIONS_&quot;);
        })
        .ConfigureAppConfiguration(configBuilder =&amp;gt;
        {
            configBuilder.AddEnvironmentVariables();

            var cmdLine = Environment.GetCommandLineArgs();
            RegisterCommandLine(configBuilder, cmdLine);
        })
        .ConfigureServices((context, services) =&amp;gt;
        {
            IFunctionsWorkerApplicationBuilder appBuilder = services.AddFunctionsWorkerDefaults(configureOptions);

            // Call the provided configuration prior to adding default middleware
            configure(context, appBuilder);

            static bool ShouldSkipDefaultWorkerMiddleware(IDictionary&amp;lt;object, object&amp;gt; props)
            {
                return props is not null &amp;amp;&amp;amp;
                    props.TryGetValue(FunctionsApplicationBuilder.SkipDefaultWorkerMiddlewareKey, out var skipObj) &amp;amp;&amp;amp;
                    skipObj is true;
            }

            if (!ShouldSkipDefaultWorkerMiddleware(context.Properties))
            {
                // Add default middleware
                appBuilder.UseDefaultWorkerMiddleware();
            }
        });

    // Invoke any extension methods auto generated by functions worker sdk.
    builder.InvokeAutoGeneratedConfigureMethods();

    return builder;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ConfigureFunctionsWebApplication&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ConfigureFunctionsWebApplication&lt;/code&gt; is defined in the &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker/blob/main/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs&quot;&gt;FunctionsHostBuilderExtensions&lt;/a&gt; class. Whiles the source code is in the same GitHub project as &lt;code&gt;ConfigureFunctionsWorkerDefaults&lt;/code&gt;, it ships as part of the &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore/&quot;&gt;Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore&lt;/a&gt; NuGet package.&lt;/p&gt;
&lt;p&gt;It too contains a number of overloads, but they all end up calling this implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder builder, Action&amp;lt;HostBuilderContext, IFunctionsWorkerApplicationBuilder&amp;gt; configureWorker)
{
    builder.ConfigureFunctionsWorkerDefaults((hostBuilderContext, workerAppBuilder) =&amp;gt;
    {
        workerAppBuilder.UseAspNetCoreIntegration();
        configureWorker?.Invoke(hostBuilderContext, workerAppBuilder);
    });

    builder.ConfigureAspNetCoreIntegration();

    return builder;
}

internal static IHostBuilder ConfigureAspNetCoreIntegration(this IHostBuilder builder)
{
    builder.ConfigureServices(services =&amp;gt;
    {
        services.AddSingleton&amp;lt;FunctionsEndpointDataSource&amp;gt;();
        services.AddSingleton&amp;lt;ExtensionTrace&amp;gt;();
        services.Configure&amp;lt;ForwardedHeadersOptions&amp;gt;(options =&amp;gt;
        {
            // By default, the X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto headers
            // are sent by the host and will be processed by the worker.
            options.ForwardedHeaders = ForwardedHeaders.All;
        });
    });

    builder.ConfigureWebHostDefaults(webBuilder =&amp;gt;
    {
        webBuilder.UseUrls(HttpUriProvider.HttpUriString);
        webBuilder.Configure(b =&amp;gt;
        {
            b.UseForwardedHeaders();
            b.UseRouting();
            b.UseMiddleware&amp;lt;WorkerRequestServicesMiddleware&amp;gt;();
            b.UseEndpoints(endpoints =&amp;gt;
            {
                var dataSource = endpoints.ServiceProvider.GetRequiredService&amp;lt;FunctionsEndpointDataSource&amp;gt;();
                endpoints.DataSources.Add(dataSource);
            });
        });
    });

    return builder;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So they&apos;re actually quite different!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I feel like the documentation suggesting using one or the other may be misleading. More likely you want to &lt;strong&gt;add&lt;/strong&gt; a call to &lt;code&gt;ConfigureFunctionsWebApplication&lt;/code&gt; but leave the call to &lt;code&gt;ConfigureFunctionsWorkerDefaults&lt;/code&gt; (unless you really want to add in all your own calls to &lt;code&gt;ConfigureHostConfiguration&lt;/code&gt; and &lt;code&gt;ConfigureAppConfiguration&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureFunctionsWorkerDefaults()
    .Build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks like &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker/issues/3010&quot;&gt;I&apos;m not alone&lt;/a&gt; with this either.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2025/02/functions-serilog-appinsights</id>
    <updated>2025-02-10T08:00:00.000+10:30</updated>
    <title>.NET Azure Functions, Isolated worker model, Serilog to App Insights</title>
    <link href="https://david.gardiner.net.au/2025/02/functions-serilog-appinsights" rel="alternate" type="text/html" title=".NET Azure Functions, Isolated worker model, Serilog to App Insights"/>
    <category term=".NET"/>
    <category term="Azure Functions"/>
    <published>2025-02-10T08:00:00.000+10:30</published>
    <summary type="html">How to configure Serilog and Application Insights in .NET Azure Functions using the isolated worker model.</summary>
    <content type="html">&lt;p&gt;There&apos;s already some good resources online about configuring &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/create-first-function-cli-csharp?WT.mc_id=DOP-MVP-5001655&quot;&gt;.NET Azure Functions&lt;/a&gt; with &lt;a href=&quot;https://serilog.net/&quot;&gt;Serilog&lt;/a&gt;. For example, Shazni gives a &lt;a href=&quot;https://medium.com/ascentic-technology/a-comprehensive-guide-to-configuring-logging-with-serilog-and-azure-app-insights-in-net-f6e4bda69e76&quot;&gt;good introduction to Serilog and then shows how to configure for in-process and isolated Azure Functions&lt;/a&gt;, and Simon shows &lt;a href=&quot;https://simonholman.dev/blog/configure-serilog-for-logging-in-azure-functions&quot;&gt;how to use Serilog with Azure Functions in isolated worker model&lt;/a&gt;, but neither cover using App Insights.&lt;/p&gt;
&lt;p&gt;It&apos;s important to note that the &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/migrate-dotnet-to-isolated-model?WT.mc_id=DOP-MVP-5001655&quot;&gt;in-process model goes out of support (along with .NET 8) in November 2026&lt;/a&gt;. Going forward, only the isolated worker model is supported by future versions of .NET (starting with .NET 9)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/serilog.DYu9tqZ__ZFlgFy.webp&quot; alt=&quot;Serilog logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Serilog Sink package for logging data to Application Insights is &lt;a href=&quot;https://www.nuget.org/packages/Serilog.Sinks.ApplicationInsights/&quot;&gt;Serilog.Extensions.AppInsights&lt;/a&gt;, and it has some useful code samples &lt;a href=&quot;https://github.com/serilog-contrib/serilog-sinks-applicationinsights&quot;&gt;in the README&lt;/a&gt;, but they also lack mentioning the differences for isolated worker model.&lt;/p&gt;
&lt;p&gt;So my goal here is to demonstrate the following combination:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A .NET Azure Function&lt;/li&gt;
&lt;li&gt;That is using isolated worker mode&lt;/li&gt;
&lt;li&gt;That logs to Azure App Insights&lt;/li&gt;
&lt;li&gt;Uses Serilog for structured logging&lt;/li&gt;
&lt;li&gt;Uses the Serilog &apos;bootstrapper&apos; pattern to capture any errors during startup/configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: There are full working samples for this post in &lt;a href=&quot;https://github.com/flcdrg/azure-function-dotnet-isolated-logging&quot;&gt;https://github.com/flcdrg/azure-function-dotnet-isolated-logging&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Our starting point is an Azure Function that has Application Insights enabled. We uncommented to the two lines in Program.cs and the two .csproj file from the default Functions project template.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =&amp;gt; {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of the challenges with using the App Insights Serilog Sink, is that it needs to be configured with an existing &lt;code&gt;TelemetryConfiguration&lt;/code&gt;. The old way of doing this was to reference &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/microsoft.applicationinsights.extensibility.telemetryconfiguration.active?view=azure-dotnet&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;TelemetryConfiguration.Active&lt;/code&gt;&lt;/a&gt;, however using this property has been discouraged in .NET Core (aka modern .NET).&lt;/p&gt;
&lt;p&gt;Instead you&apos;re encouraged to retrieve a valid &lt;code&gt;TelemetryConfiguration&lt;/code&gt; instance from the service provider, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Log.Logger = new LoggerConfiguration()
    .WriteTo.ApplicationInsights(
        serviceProvider.GetRequiredService&amp;lt;TelemetryConfiguration&amp;gt;(),
    TelemetryConverter.Traces)
    .CreateLogger();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Except we have a problem. How can we reference the service provider? We need to move this under the &lt;code&gt;HostBuilder&lt;/code&gt;, so we have access to a service provider.&lt;/p&gt;
&lt;p&gt;There&apos;s a couple of ways to do this. Traditionally we would use &lt;code&gt;UseSerilog&lt;/code&gt; to register Serilog similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    var build = Host.CreateDefaultBuilder(args)
        .UseSerilog((_, services, loggerConfiguration) =&amp;gt; loggerConfiguration
            .Enrich.FromLogContext()
            .Enrich.WithProperty(&quot;ExtraInfo&quot;, &quot;FuncWithSerilog&quot;)

            .WriteTo.ApplicationInsights(
                services.GetRequiredService&amp;lt;TelemetryConfiguration&amp;gt;(),
                TelemetryConverter.Traces))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But &lt;a href=&quot;https://nblumhardt.com/2024/04/serilog-net8-0-minimal/#comment-6496448401&quot;&gt;as of relatively recently&lt;/a&gt;, you can now also use &lt;code&gt;AddSerilog&lt;/code&gt;, as it turns out under the covers, &lt;code&gt;UseSerilog&lt;/code&gt; just calls &lt;code&gt;AddSerilog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So this is the equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services
    .AddSerilog((serviceProvider, loggerConfiguration) =&amp;gt;
    {
        loggerConfiguration
            .Enrich.FromLogContext()
            .Enrich.WithProperty(&quot;ExtraInfo&quot;, &quot;FuncWithSerilog&quot;)

            .WriteTo.ApplicationInsights(
                serviceProvider.GetRequiredService&amp;lt;TelemetryConfiguration&amp;gt;(),
                TelemetryConverter.Traces);
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s also the &apos;bootstrap logging&apos; pattern that was &lt;a href=&quot;https://nblumhardt.com/2020/10/bootstrap-logger/&quot;&gt;first outlined here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This can be useful if you want to log any configuration errors at start up. The only issue here is it will be tricky to log those into App Insights as you won&apos;t have the main Serilog configuration (where you wire up App Insights integration) completed yet. You could log to another sink (&lt;a href=&quot;https://github.com/serilog/serilog-sinks-console&quot;&gt;Console&lt;/a&gt;, or &lt;a href=&quot;https://github.com/serilog/serilog-sinks-debug&quot;&gt;Debug&lt;/a&gt; if you&apos;re running locally).&lt;/p&gt;
&lt;p&gt;Here&apos;s an example that includes bootstrap logging.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.ApplicationInsights.Extensibility;
using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.Debug()
    .CreateBootstrapLogger();

try
{
    Log.Warning(&quot;Starting up..&quot;); // Only logged to console

    var build = Host.CreateDefaultBuilder(args)
        .UseSerilog((_, services, loggerConfiguration) =&amp;gt; loggerConfiguration
            .Enrich.FromLogContext()
            .Enrich.WithProperty(&quot;ExtraInfo&quot;, &quot;FuncWithSerilog&quot;)

            .WriteTo.ApplicationInsights(
                services.GetRequiredService&amp;lt;TelemetryConfiguration&amp;gt;(),
                TelemetryConverter.Traces))

        .ConfigureFunctionsWebApplication()

        .ConfigureServices(services =&amp;gt; {
            services.AddApplicationInsightsTelemetryWorkerService();
            services.ConfigureFunctionsApplicationInsights();
        })
        .ConfigureLogging(logging =&amp;gt;
        {
            // Remove the default Application Insights logger provider so that Information logs are sent
            // https://learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide?tabs=hostbuilder%2Clinux&amp;amp;WT.mc_id=DOP-MVP-5001655#managing-log-levels
            logging.Services.Configure&amp;lt;LoggerFilterOptions&amp;gt;(options =&amp;gt;
            {
                LoggerFilterRule? defaultRule = options.Rules.FirstOrDefault(rule =&amp;gt; rule.ProviderName
                    == &quot;Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider&quot;);
                if (defaultRule is not null)
                {
                    options.Rules.Remove(defaultRule);
                }
            });
        })

        .Build();

    build.Run();
    Log.Warning(&quot;After run&quot;);
}
catch (Exception ex)
{
    Log.Fatal(ex, &quot;An unhandled exception occurred during bootstrapping&quot;);
}
finally
{
    Log.Warning(&quot;Exiting application&quot;);
    Log.CloseAndFlush();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my experimenting with this, when the Function is closed normally (eg. by being requested to stop in the Azure Portal / or pressing Ctrl-C in the console window when running locally) I was not able to get any logging working in the &lt;code&gt;finally&lt;/code&gt; block. I think by then it&apos;s pretty much game over and the Function Host is keen to wrap things up.&lt;/p&gt;
&lt;p&gt;But what if the Function is running in Azure? The Debug or Console sinks won&apos;t be much use there. In ApplicationInsights sink docs, there&apos;s a section on &lt;a href=&quot;https://github.com/serilog-contrib/serilog-sinks-applicationinsights#how-when-and-why-to-flush-messages-manually&quot;&gt;how to flush messages manually&lt;/a&gt;. The code sample shows creating a new instance of &lt;code&gt;TelemetryClient&lt;/code&gt; so that you &lt;em&gt;can&lt;/em&gt; use the ApplicationInsights sink in the bootstrap logger.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.Debug()
    .WriteTo.ApplicationInsights(new TelemetryClient(new TelemetryConfiguration()), new TraceTelemetryConverter())
    .CreateBootstrapLogger();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I simulate a configuration error by throwing an exception inside the &lt;code&gt;ConfigureServices&lt;/code&gt; call, then you do get data sent to App Insights. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;AppExceptions&quot;,
    &quot;time&quot;: &quot;2025-02-08T06:32:25.4548247Z&quot;,
    &quot;tags&quot;: {
        &quot;ai.cloud.roleInstance&quot;: &quot;Delphinium&quot;,
        &quot;ai.internal.sdkVersion&quot;: &quot;dotnetc:2.22.0-997&quot;
    },
    &quot;data&quot;: {
        &quot;baseType&quot;: &quot;ExceptionData&quot;,
        &quot;baseData&quot;: {
            &quot;ver&quot;: 2,
            &quot;exceptions&quot;: [
                {
                    &quot;id&quot;: 59941933,
                    &quot;outerId&quot;: 0,
                    &quot;typeName&quot;: &quot;System.InvalidOperationException&quot;,
                    &quot;message&quot;: &quot;This is a test exception&quot;,
                    &quot;hasFullStack&quot;: true,
                    &quot;parsedStack&quot;: [
                        {
                            &quot;level&quot;: 0,
                            &quot;method&quot;: &quot;Program+&amp;lt;&amp;gt;c.&amp;lt;&amp;lt;Main&amp;gt;$&amp;gt;b__0_1&quot;,
                            &quot;assembly&quot;: &quot;FuncWithSerilog, Version=1.2.6.0, Culture=neutral, PublicKeyToken=null&quot;,
                            &quot;fileName&quot;: &quot;D:\\git\\azure-function-dotnet-isolated-logging\\net9\\FuncWithSerilog\\Program.cs&quot;,
                            &quot;line&quot;: 36
                        },
                        {
                            &quot;level&quot;: 1,
                            &quot;method&quot;: &quot;Microsoft.Extensions.Hosting.HostBuilder.InitializeServiceProvider&quot;,
                            &quot;assembly&quot;: &quot;Microsoft.Extensions.Hosting, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60&quot;,
                            &quot;line&quot;: 0
                        },
                        {
                            &quot;level&quot;: 2,
                            &quot;method&quot;: &quot;Microsoft.Extensions.Hosting.HostBuilder.Build&quot;,
                            &quot;assembly&quot;: &quot;Microsoft.Extensions.Hosting, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60&quot;,
                            &quot;line&quot;: 0
                        },
                        {
                            &quot;level&quot;: 3,
                            &quot;method&quot;: &quot;Program.&amp;lt;Main&amp;gt;$&quot;,
                            &quot;assembly&quot;: &quot;FuncWithSerilog, Version=1.2.6.0, Culture=neutral, PublicKeyToken=null&quot;,
                            &quot;fileName&quot;: &quot;D:\\git\\azure-function-dotnet-isolated-logging\\net9\\FuncWithSerilog\\Program.cs&quot;,
                            &quot;line&quot;: 21
                        }
                    ]
                }
            ],
            &quot;severityLevel&quot;: &quot;Critical&quot;,
            &quot;properties&quot;: {
                &quot;MessageTemplate&quot;: &quot;An unhandled exception occurred during bootstrapping&quot;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So there you go!&lt;/p&gt;
&lt;p&gt;And this is all well and good, but it&apos;s important to mention that Microsoft are suggesting for new codebases &lt;a href=&quot;https://learn.microsoft.com/azure/azure-monitor/app/worker-service?WT.mc_id=DOP-MVP-5001655&quot;&gt;use OpenTelemetry instead of App Insights&lt;/a&gt;! I&apos;ll have to check out how that works soon.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/serilog.DYu9tqZ_.png" width="200" height="200"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/serilog.DYu9tqZ_.png" width="200" height="200"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2024/01/cfs-azure-function</id>
    <updated>2024-01-13T13:00:00.000+10:30</updated>
    <title>Azure Function posting an RSS feed to Mastodon</title>
    <link href="https://david.gardiner.net.au/2024/01/cfs-azure-function" rel="alternate" type="text/html" title="Azure Function posting an RSS feed to Mastodon"/>
    <category term="Azure"/>
    <category term="Azure Functions"/>
    <category term=".NET"/>
    <published>2024-01-13T13:00:00.000+10:30</published>
    <summary type="html">How to use an Azure Function and C# script to poll an RSS feed and post new entries to Mastodon.</summary>
    <content type="html">&lt;p&gt;Twitter (or &apos;X&apos; as it seems to be called now), to my surprise, hasn&apos;t died yet. &lt;a href=&quot;https://twitter.com/DavidRGardiner&quot;&gt;I&apos;m still there&lt;/a&gt;, but I must say I&apos;m enjoying the discussions over on Mastodon a lot more (follow me at &lt;a href=&quot;https://mastodon.online/@DavidRGardiner&quot;&gt;https://mastodon.online/@DavidRGardiner&lt;/a&gt;). But there are a few feeds that I follow on Twitter that I&apos;d like to follow on Mastodon. So I wrote a little Azure Function to do that for me (and anyone else who is interested).&lt;/p&gt;
&lt;p&gt;One that is relevant to living in South Australia, especially over the warmer months, given where I live is a bushfire-prone area, is keeping an eye on the updates from the &lt;a href=&quot;https://cfs.sa.gov.au/home/&quot;&gt;Country Fire Service&lt;/a&gt; (known locally as the CFS). They have a Twitter account, but not a Mastodon account. If only there was a way to get their updates on Mastodon!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/cfs-alert.BOlotj3W_14HxRW.webp&quot; alt=&quot;Example CFS Alert posted to Mastodon&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As it turns out, the CFS &lt;a href=&quot;https://cfs.sa.gov.au/warnings-restrictions/warnings/rss-feeds/&quot;&gt;publish some RSS feeds&lt;/a&gt;. My first attempt was to make use of a service like &lt;a href=&quot;https://mastofeed.org&quot;&gt;Mastofeed&lt;/a&gt;, which in theory can take an RSS feed and post updates to Mastodon. But as I discovered, the RSS feed from the CFS has a few oddities that seem to prevent this from working correctly - it posted one update but then stopped. Here&apos;s an example of the RSS feed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&apos;1.0&apos; ?&amp;gt;
&amp;lt;rss version=&apos;2.0&apos; xmlns:atom=&apos;http://www.w3.org/2005/Atom&apos;&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;atom:link href=&apos;https://data.eso.sa.gov.au/prod/cfs/criimson/cfs_current_incidents.xml&apos; rel=&apos;self&apos; type=&apos;application/rss+xml&apos; /&amp;gt;
    &amp;lt;ttl&amp;gt;15&amp;lt;/ttl&amp;gt;
    &amp;lt;title&amp;gt;Country Fire Service - South Australia - Current Incidents&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;https://www.cfs.sa.gov.au/incidents/&amp;lt;/link&amp;gt;
    &amp;lt;description&amp;gt;Current Incidents&amp;lt;/description&amp;gt;
    &amp;lt;item&amp;gt;
      &amp;lt;link&amp;gt;https://www.cfs.sa.gov.au/incidents/&amp;lt;/link&amp;gt;
      &amp;lt;guid isPermaLink=&apos;false&apos;&amp;gt;https://data.eso.sa.gov.au/prod/cfs/criimson/1567212.&amp;lt;/guid&amp;gt;
      &amp;lt;title&amp;gt;TIERS ROAD, LENSWOOD (Tree Down)&amp;lt;/title&amp;gt;
      &amp;lt;identifier&amp;gt;1567212&amp;lt;/identifier&amp;gt;
      &amp;lt;description&amp;gt;First Reported: Saturday, 06 Jan 2024 15:41:00&amp;amp;lt;br&amp;amp;gt;Status: GOING&amp;amp;lt;br&amp;amp;gt;Region: 1&amp;lt;/description&amp;gt;
      &amp;lt;pubDate&amp;gt;Sat, 06 Jan 2024 16:15:03 +1030&amp;lt;/pubDate&amp;gt;
    &amp;lt;/item&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is the &lt;code&gt;identifier&lt;/code&gt; element that is non-standard, but I suspect the main issue is the &lt;code&gt;guid&lt;/code&gt; element. For some reason, the &lt;code&gt;isPermaLink&lt;/code&gt; attribute is set to false. On the face of it, that looks like a mistake. That URI (which incorporates the identifier value) does appear to be unique. With &lt;code&gt;isPermaLink&lt;/code&gt; set to false, it hints that the value can&apos;t be used as a unique identifier. I&apos;m guessing because of that, Mastofeed had no way to distinguish posts in the RSS feed.&lt;/p&gt;
&lt;p&gt;So we&apos;re out of luck using the simple option. My next thought was whether there was something that could transform/fix up the RSS on the fly - an &apos;XSLT proxy&apos; if you like, but I&apos;ve not found a free offering like that.&lt;/p&gt;
&lt;p&gt;Maybe I can write some code to do the job instead. Hosting it in an Azure Function should work, and ideally would be free (or really cheap).&lt;/p&gt;
&lt;h2&gt;An Azure Function&lt;/h2&gt;
&lt;p&gt;I ended up writing a relatively simple Azure Function in C# that polls the RSS feed every 15 minutes (as that&apos;s the value of the &lt;code&gt;ttl&lt;/code&gt; element). It then posts any new items to Mastodon. Here&apos;s the code (&lt;a href=&quot;https://github.com/flcdrg/cfsalerts-mastodon/blob/be9f1ddde2e121e047d9d6ed45a141263d05db58/src/CfsAlerts/CfsFunction.cs&quot;&gt;Link to GitHub repo&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    [Function(nameof(CheckAlerts))]
    public async Task&amp;lt;List&amp;lt;CfsFeedItem&amp;gt;&amp;gt; CheckAlerts([ActivityTrigger] List&amp;lt;CfsFeedItem&amp;gt; oldList)
    {
        var newList = new List&amp;lt;CfsFeedItem&amp;gt;();

        var response = string.Empty;
        try
        {
            using var httpClient = _httpClientFactory.CreateClient();

            response = await httpClient.GetStringAsync(
                &quot;https://data.eso.sa.gov.au/prod/cfs/criimson/cfs_current_incidents.xml&quot;);

            var xml = XDocument.Parse(response);

            if (xml.Root.Element(&quot;channel&quot;) is null)
                throw new InvalidOperationException(&quot;No channel element found in feed&quot;);

            var xmlItems = xml.Root.Element(&quot;channel&quot;)?.Elements(&quot;item&quot;).ToList();

            if (xmlItems is not null)
                foreach (var item in xmlItems)
                {
                    var dateTime = DateTime.Parse(item.Element(&quot;pubDate&quot;).Value);

                    newList.Add(new CfsFeedItem(
                        item.Element(&quot;guid&quot;).Value,
                        item.Element(&quot;title&quot;).Value,
                        item.Element(&quot;description&quot;).Value,
                        item.Element(&quot;link&quot;).Value,
                        dateTime
                    ));
                }

            // Find items in newList that are not in oldList
            var newItems = newList.Except(oldList).ToList();

            if (newItems.Any())
            {
                var accessToken = _settings.Token;
                var client = new MastodonClient(_settings.Instance, accessToken);

                foreach (var item in newItems)
                {
                    var message = $&quot;{item.Title}\n\n{item.Description.Replace(&quot;&amp;lt;br&amp;gt;&quot;, &quot;\n&quot;)}\n{item.Link}&quot;;

                    _logger.LogInformation(&quot;Tooting: {item}&quot;, message);

#if RELEASE
                await client.PublishStatus(message, Visibility.Unlisted);
#endif
                }
            }
            else
            {
                _logger.LogInformation(&quot;No new items found&quot;);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Problems. Data: {data}&quot;, response);
        }

        return newList;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We keep a copy of the previous list of items, and then compare it to the new list. If there are any new items, we post them to Mastodon. Because we need to remember the previous run items, we need to use &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview?tabs=csharp&quot;&gt;Durable Functions&lt;/a&gt;. This was my first time creating a Durable Function, so that also made it a good learning experience. The &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-eternal-orchestrations?WT.mc_id=DOP-MVP-5001655&quot;&gt;&apos;eternal orchestration&apos; pattern&lt;/a&gt; is used, where the orchestration function calls itself, passing in the new list of items. Here&apos;s the orchestration function code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    [Function(nameof(MonitorJobStatus))]
    public static async Task Run(
        [OrchestrationTrigger] TaskOrchestrationContext context, List&amp;lt;CfsFeedItem&amp;gt; lastValue)
    {
        var newValue = await context.CallActivityAsync&amp;lt;List&amp;lt;CfsFeedItem&amp;gt;&amp;gt;(nameof(CfsFunction.CheckAlerts), lastValue);

#if RELEASE
        // Orchestration sleeps until this time (TTL is 15 minutes in RSS)
        var nextCheck = context.CurrentUtcDateTime.AddMinutes(15);
#else
        var nextCheck = context.CurrentUtcDateTime.AddSeconds(20);
#endif

        await context.CreateTimer(nextCheck, CancellationToken.None);

        context.ContinueAsNew(newValue);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Durable Function infrastructure handles serialising and deserialising the list of items automatically. The Function takes a parameter that receives the list from the previous run. When the function completes, it returns the list that will be passed to the next run. The orchestration function passes in the list and sets things up to save the list so that it can be passed to the next run.&lt;/p&gt;
&lt;p&gt;One thing about this pattern is you need some way of kickstarting the process. The way I chose was to add a HTTP trigger function that calls the orchestration function. The release pipeline makes a call to the HTTP trigger endpoint after it publishes the function.&lt;/p&gt;
&lt;h2&gt;Durable + .NET 8 + isolated&lt;/h2&gt;
&lt;p&gt;The Durable Function targets .NET 8 and uses the isolated model. It was a little challenging figuring out how to get this combination to work, as most of the documentation is either for the in-process model or for earlier versions of .NET. Ensuring that the appropriate NuGet packages were being referenced was tricky, as there are often different packages to use depending on the model and version of .NET. I ended up using the following packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;PackageReference Include=&quot;Mastonet&quot; Version=&quot;2.3.1&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Azure.Functions.Worker&quot; Version=&quot;1.20.0&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Azure.Functions.Worker.Extensions.DurableTask&quot; Version=&quot;1.1.0&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Azure.Functions.Worker.Extensions.Http&quot; Version=&quot;3.1.0&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Azure.Functions.Worker.Extensions.Timer&quot; Version=&quot;4.3.0&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Azure.Functions.Worker.Sdk&quot; Version=&quot;1.16.4&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.Extensions.Configuration.UserSecrets&quot; Version=&quot;8.0.0&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Costs&lt;/h2&gt;
&lt;p&gt;To keep costs to a minimum, the Azure Function is running on a consumption plan. The intention is to keep it close to or under the &lt;a href=&quot;https://azure.microsoft.com/en-au/pricing/details/functions/&quot;&gt;free threshold&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As it turns out, so far the Function is not costing very much at all. It&apos;s actually the Storage Account that is the most significant. AUD8.30 so far and the total forecast for the month will be AUD17. An Azure Function needs to be linked to a Storage Account. It would be interesting to see if there are any changes I could make to reduce the cost further.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/cfs-azure-costs.DbgQn2Pm_1DCXII.webp&quot; alt=&quot;Azure cost summary&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To see the latest posts from the Azure Function, you can go to &lt;a href=&quot;https://mastodon.online/@CFSAlerts&quot;&gt;https://mastodon.online/@CFSAlerts&lt;/a&gt; (and if you&apos;re on Mastodon, feel free to follow the account!)&lt;/p&gt;
&lt;h2&gt;Future enhancements&lt;/h2&gt;
&lt;p&gt;Apart from seeing if I can reduce the cost even more, the other thing that would be useful is to also track the daily fire bans. This data is &lt;a href=&quot;https://data.eso.sa.gov.au/prod/cfs/criimson/fireDangerRating.xml&quot;&gt;published as an XML file&lt;/a&gt;, so parsing that once a day should be pretty straightforward.&lt;/p&gt;
&lt;p&gt;You can find the full source code for the Azure Function in &lt;a href=&quot;https://github.com/flcdrg/cfsalerts-mastodon/&quot;&gt;this GitHub repo&lt;/a&gt;.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2021/08/azure-functions-specific-functions</id>
    <updated>2021-08-04T13:52:00.000+09:30</updated>
    <title>Azure Functions - Enable specific functions for debugging</title>
    <link href="https://david.gardiner.net.au/2021/08/azure-functions-specific-functions" rel="alternate" type="text/html" title="Azure Functions - Enable specific functions for debugging"/>
    <category term="Azure"/>
    <category term="Azure Functions"/>
    <published>2021-08-04T13:52:00.000+09:30</published>
    <summary type="html">I&apos;ve been using Azure Functions quite a bit.</summary>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX_2tR59E.webp&quot; alt=&quot;Azure Functions logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve been using &lt;a href=&quot;https://learn.microsoft.com/azure/azure-functions/&quot;&gt;Azure Functions&lt;/a&gt; quite a bit. Indeed I&apos;ve been recently &lt;a href=&quot;https://www.meetup.com/Adelaide-dotNET/events/278946503/&quot;&gt;speaking&lt;/a&gt; about &lt;a href=&quot;https://www.meetup.com/Melb-NET-Meetup/events/279602669/&quot;&gt;them&lt;/a&gt; and the new support for .NET 5 and soon .NET 6!&lt;/p&gt;
&lt;p&gt;Today I wanted to debug an Azure Function application, but I didn&apos;t want all of the functions in the application to run (as some of them access resources that I don&apos;t have locally). I discovered that you can add a &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json#functions&quot;&gt;&lt;code&gt;functions&lt;/code&gt;&lt;/a&gt; property to your &lt;code&gt;host.json&lt;/code&gt; file to list the specific functions that should run.&lt;/p&gt;
&lt;p&gt;eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;2.0&quot;,
  &quot;functions&quot;: [
    &quot;function1&quot;,
    &quot;function2&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But I really would prefer not to edit &lt;code&gt;host.json&lt;/code&gt; as that file is under source control and I&apos;d then need to remember to not commit those changes. I&apos;d much prefer not to have to remember too many things!&lt;/p&gt;
&lt;p&gt;There&apos;s a &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json#override-hostjson-values&quot;&gt;section in the documentation&lt;/a&gt; that describes how you can also set these values in your &lt;code&gt;local.settings.json&lt;/code&gt; (which isn&apos;t usually under source control). But the examples given are for simple boolean properties. How do you enter the array value?&lt;/p&gt;
&lt;p&gt;To find out, I temporarily set the values in &lt;code&gt;host.json&lt;/code&gt; and used the &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.configurationrootextensions.getdebugview?view=dotnet-plat-ext-5.0#Microsoft_Extensions_Configuration_ConfigurationRootExtensions_GetDebugView_Microsoft_Extensions_Configuration_IConfigurationRoot_&quot;&gt;&lt;code&gt;IConfigurationRoot.GetDebugView()&lt;/code&gt;&lt;/a&gt; extension method to dump out all the configuration to see how they were represented.&lt;/p&gt;
&lt;p&gt;Here&apos;s the answer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;Values&quot;: {
        &quot;AzureFunctionsJobHost__functions__0&quot;: &quot;function1&quot;,
        &quot;AzureFunctionsJobHost__functions__1&quot;: &quot;function2&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;__0&lt;/code&gt; and &lt;code&gt;__1&lt;/code&gt; represent each element in the array. With that in place when I run the Azure Function locally, only &lt;code&gt;function&lt;/code&gt; and &lt;code&gt;function2&lt;/code&gt; will run. All others will be ignored. Just add additional properties (incrementing the number) to enable more functions.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/azure-function.B3FwAwqX.png" width="400" height="400"/>
  </entry>
</feed>
