Using IConfiguration and appsettings.json in a .NET Core Azure Function project

For very basic Web APIs, HTTP triggered Azure functions are a very efficient and cost effective way to implement the service. One inconvenience though is that by default it does not come hooked up with an appsettings.json file or an IConfiguration instance. Fortunately, it’s not very difficult to hook those up.

Step 1

Add the following NuGet package to your project.

  • Microsoft.Azure.Functions.Extensions

Step 2

Add a startup.cs file to your project and insert the following code.

[assembly: FunctionsStartup(typeof(Startup))]

public class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(
      IFunctionsConfigurationBuilder builder)
    {
        var context = builder.GetContext();

        builder.ConfigurationBuilder
            .AddJsonFile(
              Path.Combine(
                context.ApplicationRootPath, 
                "appsettings.json"), 
              optional: true, 
              reloadOnChange: false)
            .AddJsonFile(
              Path.Combine(
                context.ApplicationRootPath, 
                $"appsettings.{context.EnvironmentName}.json"), 
              optional: true, 
              reloadOnChange: false)
            .AddEnvironmentVariables();
    }
}

Step 3

Add your appsettings.json file and setup any configuration properties you need in there.

{
  "YourProperty": "some-value-here"
}

Step 4

Inject IConfiguration into your constructor. Note that the default function template won’t have a constructor as it uses a static class. Remove the static, and add the constructor.

private readonly IConfiguration _configuration;

public SomeFunction(IConfiguration configuration)
{
    _configuration = configuration;
}

And now you can use it in the function as you’d normally do.

[FunctionName("SomeFunction")]
public async Task<IActionResult> Run(
    [HttpTrigger(
      AuthorizationLevel.Anonymous, 
      "get", 
      "post", 
      Route = null)] 
    HttpRequest req,
    ILogger log)
{
  // Use _configuration["YourProperty"]
}

Using Newtonsoft JsonConvert to easily construct a derived class from a base instance

Okay, so this may not be the most efficient code – so you may not want to use this in a context where milli seconds matter. But say you have a model class (returned by another library or from an API) and you need to extend it by adding a single property to it. The simplest way to do this is to derive a new class, add the property to it and then to add a constructor that copy-constructs from base to derived. Bit of a hassle having to copy every single property though, although I’d imagine some of those new AI powered IDEs could auto-generate code for you. Even then, if the base class ever changes, you will need to update this constructor and if you forget to do that, there’s a risk of random buggy behavior. So, what’s the hack? Use JsonConvert to serialize the base object, then deserialize it to derived and then populate the new field or fields that’s been added to derived.

static void Main()
{
    ModelEx model = Foo(new Model
    {
        Id = 1000,
        Name = "Mary Miller",
        Description = "Senior Accountant"
    });
    model.Enabled = true;
}

private static ModelEx Foo(Model model)
{
    var json = JsonConvert.SerializeObject(model);
    return JsonConvert.DeserializeObject<ModelEx>(json);
}

class ModelEx : Model
{
    public bool Enabled { get; set; }
}

class Model
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

3 lines of code for an ASP.NET Web API

Okay, this is admittedly a bit of a frivolous post. That said, with just these 3 lines of code, you will have a REST API up and running.

var app = WebApplication.Create();
app.MapGet("/", () => $"Running @ {DateTime.UtcNow}");
await app.RunAsync();

Line 1 instantiates your web app instance, the 2nd line adds your one endpoint, and the last one starts the web app (http listener).

Exiting an Azure WebJob programmatically

If you need your WebJob to run a task and exit, or if you need to conditionally exit it under certain scenarios, and you thought it’s just a matter of exiting the worker service’s ExecuteAsync method, you’d be wrong. Even after your worker code has stopped running, the parent host stays alive. The right way to exit your WebJob is to inject IHostApplicationLifetime into your worker, and then the last line of code in your ExecuteAsync would be a call to its StopApplication method.

public Worker(ILogger<Worker> logger, IHostApplicationLifetime appLifeTime)
{
    _logger = logger;
    _appLifetime = appLifeTime;            
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    int runs = 0;

    while (!stoppingToken.IsCancellationRequested && runs++ < 3)
    {
        // do some task here   
        await Task.Delay(2000, stoppingToken);
    }

    _appLifetime.StopApplication();
}

Azure WebJobs not logging to Application Insights

With regular App Services, if you enabled Application Insights, your ILogger logs will get written into Application Insights without having to do anything special. This is not the case with WebJobs though. After wasting a few hours trying to figure out why my logs were not showing up, here’s a solution I found that worked.

Step 1

Install the latest version of Microsoft.Azure.WebJobs.Logging.ApplicationInsights.

Step 2

Modify your startup code as follows.

public static void Main(string[] args)
{
    IHost host = Host.CreateDefaultBuilder(args)
        .ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        })
        .ConfigureLogging((context, builder) =>
        {
            builder.AddConsole();

            var key = context.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
            if (!string.IsNullOrEmpty(key))
            {
                builder.AddApplicationInsightsWebJobs(
                    options => options.InstrumentationKey = key);
            }
        })
        .Build();

    host.Run();
}

And that’s it. Remember that there’s usually a 1-2 minute delay before your logs show up in App Insights, and it could be as high as 3-4 minutes if it’s the first time you deploy. So, don’t panic if you don’t see the logs right away. Happy debugging!

Serializing enums as strings

Enumerations in C# are ints by default, which is fine most of the time. That said, there are situations where you’d want to store and retrieve them using their string names, especially when you are persisting them to a database. The string names make them more readable for one reason. For another, a re-ordering of the enums (caused inadvertently by the addition or removal of an entry) would change their int values, which can potentially break your data when reading them back from a database. Consider the following types – Model and Seasons.

enum Seasons
{
    Spring,
    Summer,
    Fall,
    Winter
}

class Model
{
    public string Name { get; set; } = String.Empty;

    public Seasons Season { get; set; }
}

If you serialize an instance of Model, you’d see the int value for the Season property.

var model = new Model
{
    Name = "Test Model",
    Season = Seasons.Fall,
};

var json  = JsonConvert.SerializeObject(model, Formatting.Indented);
Console.WriteLine(json);

Output looks like this.

{
  "Name": "Test Model",
  "Season": 2
}

It’s fairly easy to change this behavior though. Just add a JsonConverter attribute to the Season property.

class Model
{
    public string Name { get; set; } = String.Empty;

    [JsonConverter(typeof(StringEnumConverter))]
    public Seasons Season { get; set; }
}

Output is now nice and clean.

{
  "Name": "Test Model",
  "Season": "Fall"
}

You get the same behavior both when serializing and deserializing, and the latter is important when reading data back from a database.

How about if you have a collection of enums though? That’s simple too, just replace the JsonConverter attribute with the following JsonProperty attribute.

class Model
{
    public string Name { get; set; } = String.Empty;

    [JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
    public List<Seasons> Seasons { get; set; } = new List<Seasons>();
}

Output is as expected.

{
  "Name": "Test Model",
  "Seasons": [
    "Fall",
    "Winter"
  ]
}

.NET Core OData client using Microsoft.OData.Client

If you are using .NET Core, you’ll have quickly found that the .NET OData packages don’t work with it. Fortunately, the most recent (as of today) version of Microsoft.OData.Client (v7.5.0) works fine with .NET Core. Once you add the package to your project, you get a T4 file generated for you (.tt extension). Edit the Configuration class in the file and put in your connection info.

public static class Configuration
{
  public const string MetadataDocumentUri = "http://localhost:58200/odata/";

  public const bool UseDataServiceCollection = true;

  public const string NamespacePrefix = "ConsoleAppClient";

  public const string TargetLanguage = "CSharp";

  public const bool EnableNamingAlias = true;

  public const bool IgnoreUnexpectedElementsAndAttributes = true;
}

Now you can use the auto-generated Container class to make OData calls against any OData service. Here’s some example usage.

var container = new Container(new Uri("http://localhost:58200/odata/"));

var response = await container.Clients.ExecuteAsync();

var query = (DataServiceQuery)container
    .Clients
    .Expand("Address")
    .Where(it => it.Id > 10);

var resp2 = await query.ExecuteAsync();
foreach (Client item in resp2)
{
    Console.WriteLine(item.Address.City);
}

foreach (Client item in response)
{
    Console.WriteLine(item.Name);
}

var c = container.Clients.ByKey(100);
var s = await c.NameLength().GetValueAsync();
Console.WriteLine(s);

If you look at your IIS logs, you’ll see corresponding OData queries in there.

GET /odata/Clients - 58200 - ::1 Microsoft.OData.Client/7.5.0 - 200 0 0 31
GET /odata/Clients $filter=Id gt 10&$expand=Address 58200 - ::1 Microsoft.OData.Client/7.5.0 - 200 0 0 32
GET /odata/Clients(100)/Default.NameLength() - 58200 - ::1 Microsoft.OData.Client/7.5.0 - 200 0 0 130

ASP.NET Core, OData, and Swashbuckle – workaround for error

If you are trying to use Swashbuckle with an ASP.NET Core project that uses OData, you are going to get an error on the swagger endpoint. The error will be something like this.

InvalidOperationException: No media types found in ‘Microsoft.AspNet.OData.Formatter.ODataOutputFormatter.SupportedMediaTypes’. Add at least one media type to the list of supported media types.

Until one of these guys between them fix this, here’s the hackish fix. It won’t enable Swagger for the OData controllers, but it will stop Swagger from breaking for the other controllers.

services.AddMvc(op =>
{
    foreach (var formatter in op.OutputFormatters
        .OfType<ODataOutputFormatter>()
        .Where(it => !it.SupportedMediaTypes.Any()))
    {
        formatter.SupportedMediaTypes.Add(
            new MediaTypeHeaderValue("application/prs.mock-odata"));
    }
    foreach (var formatter in op.InputFormatters
        .OfType<ODataInputFormatter>()
        .Where(it => !it.SupportedMediaTypes.Any()))
    {
        formatter.SupportedMediaTypes.Add(
            new MediaTypeHeaderValue("application/prs.mock-odata"));
    }
});

OData with ASP.NET Core

If you are writing .NET Core REST services to expose data entities, I would recommend using OData now that ASP.NET Core support has been added. You need to add the Nuget package – Microsoft.AspNetCore.OData (at the time of writing, the latest stable version is 7.0.1). One caveat is that Swagger and Swashbuckle will not work with your OData controllers. The issue has been reported on the Swashbuckle git forums, and hopefully it will be resolved in the not too far future. I quickly tested out a demo app and it seemed to work as expected. Here are the model classes.

public class Client
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public Address Address { get; set; }
}

public class Address
{
    [Key]
    public int Id { get; set; }

    public string Street { get; set; }

    public string City { get; set; }

    public string State { get; set; }
}

Here’s what you add to ConfigureServices. Add it before the call to AddMvc.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ClientContext>(op =>
    {
        op.UseInMemoryDatabase("clientsDatabase");
    });

    services.AddOData();

And here are the changes made to the Configure method. Those are extension methods added by the OData package, and essentially turns on those OData features on your service. Note that I’ve added two entity sets as well as a function on one of the entities.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMvc(rb => 
    {
        rb.Expand().OrderBy().Select().Filter();
        rb.MapODataServiceRoute("odata", "odata", BuildEdmModel());
    });
}

public static IEdmModel BuildEdmModel()
{
    var builder = new ODataConventionModelBuilder();
    builder.EntitySet<Client>("Clients");
    builder.EntitySet<Address>("Addresses");
    builder.EntityType<Client>()
        .Function("NameLength")
        .Returns<string>();
    return builder.GetEdmModel();
}

And this is my mocked up data context class (based off EF Core).

public class ClientContext : DbContext
{
    public ClientContext(DbContextOptions<ClientContext> options) : base(options)
    {
    }

    public DbSet<Client> Clients { get; set; }

    public DbSet<Address> Addresses { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }

    public void InitializeIfEmpty()
    {
        if (this.Clients.Count() == 0)
        {
            var address = new Address()
            {
                Id = 25,
                Street = "100 Main St.",
                City = "Columbus",
                State = "OH"
            };

            this.Addresses.Add(address);

            this.Clients.Add(new Client()
            {
                Id = 100,
                Name = "Sam Slick",
                Address = address
            });

            this.Clients.Add(new Client()
            {
                Id = 105,
                Name = "Janet Slick",
                Address = address
            });

            this.SaveChanges();
        }
    }
}

This is how we setup a Clients endpoint.

public class ClientsController : ODataController
{
    private ClientContext _dbContext;

    public ClientsController(ClientContext context)
    {
        _dbContext = context;

        _dbContext.InitializeIfEmpty();
    }

Adding methods to pull all clients, a specific client, and a specific client’s address.

[EnableQuery]
public IActionResult Get()
{
    return Ok(_dbContext.Clients);
}

[EnableQuery]
public IActionResult Get(int key)
{
    Client client = _dbContext.Clients
        .Include(c => c.Address)
        .FirstOrDefault(it => it.Id == key);

    return Ok(client);
}

[EnableQuery]
public IActionResult GetAddress([FromODataUri]int key)
{
    Client client = _dbContext.Clients
        .Include(c => c.Address)
        .FirstOrDefault(it => it.Id == key);

    return base.Ok(client.Address);
}

Here’s how you would add a Post action.

[EnableQuery]
public IActionResult Post([FromBody] Client client)
{
    _dbContext.Add(client);
    _dbContext.SaveChanges();
    return Created(client);
}

And here’s how you expose the OData action.

public IActionResult NameLength([FromODataUri]int key)
{
    var client = _dbContext.Clients.FirstOrDefault(it => it.Id == key);

    return Ok($"{client.Name} -> {client.Name.Length}");
}

That’s all for the demo. Now you can make standard OData queries against this API.

http://localhost:52913/odata/clients
http://localhost:52913/odata/clients?$expand=Address
http://localhost:52913/odata/clients/105/address
http://localhost:52913/odata/clients/105/NameLength