Simplifying Parameter Null and Other Checks with the GuardClauses Library

Often you want to add null checking and other check code at the start of a method to ensure all the values passed into the method are valid before continuing.

For example the following method checks the name and age:

public static void AddNewPerson(string name, int ageInYears)
{
    if (string.IsNullOrWhiteSpace(name))
    {
        throw new ArgumentException($"Cannot be null, empty, or contain only whitespace.", nameof(name));
    }

    if (ageInYears < 1)
    {
        throw new ArgumentOutOfRangeException(nameof(ageInYears), "Must be greater than zero.");
    }

    // Add to database etc.
}

This “guard” kind of code can “clutter” the method and reduce readability.

One library I recently came across is the Guard Clauses library from Steve Smith.

Once this library is installed we could refactor the preceding code to look like the following:

public static void AddNewPerson(string name, int ageInYears)
{
    Guard.Against.NullOrWhiteSpace(name, nameof(name));
    Guard.Against.NegativeOrZero(ageInYears, nameof(ageInYears));

    // Add to database etc.
}

Passing a null name results in the exception: System.ArgumentNullException: Value cannot be null. (Parameter 'name')

Passing an empty string results in: System.ArgumentException: Required input name was empty. (Parameter 'name')

Passing in an age of zero results in: System.ArgumentException: Required input ageInYears cannot be zero or negative. (Parameter 'ageInYears')

The code is also more readable and succinct.

Out of the box the library comes with the following guards (taken from the documentation):

  • Guard.Against.Null (throws if input is null)
  • Guard.Against.NullOrEmpty (throws if string or array input is null or empty)
  • Guard.Against.NullOrWhiteSpace (throws if string input is null, empty or whitespace)
  • Guard.Against.OutOfRange (throws if integer/DateTime/enum input is outside a provided range)
  • Guard.Against.OutOfSQLDateRange (throws if DateTime input is outside the valid range of SQL Server DateTime values)
  • Guard.Against.Zero (throws if number input is zero)

You can also define your own reusable clauses:

// Define in this namespace so can use alongside built-in guards with no additional namespaces required
namespace Ardalis.GuardClauses
{
    public static class PositiveGuard
    {
        public static void Positive(this IGuardClause guardClause, int input, string parameterName)
        {
            if (input >= 0)
            {
                throw new ArgumentOutOfRangeException(parameterName, $"Required input {parameterName} cannot be positive.");
            }                           
        }
    }
}

And then in a method we can write:

public static void ReportNegativeTemperature(int temp)
{
    Guard.Against.Positive(temp, nameof(temp));
    // Do something
}

And if we pass a positive (or zero) temp we get: System.ArgumentOutOfRangeException: Required input temp cannot be positive. (Parameter 'temp')

This is one of those simple libraries that can make basic tasks easier/more readable.

If you check this out and use it make sure you say thanks to Steve on Twitter and let him know @robertsjason sent you ;)

SHARE:

Writing Azure Functions with Function Monkey: Using Commands Without Handlers

If you’ve read the previous articles on Function Monkey you may be wondering if you always need a command handler. Sometimes you may want to accept a request into the system (for example via HTTP) and that pass that request off for further processing. For example the HTTP data can be accepted and then the data (“command”) can be put on a queue for processing. This allows the potential scale-out of the function that processes queue messages to improve the overall throughput of the system.

Take the following example that allows an invoice to be submitted via HTTP. The submitted invoice is validated before simply being returned from the SubmitInvoiceCommandHandler. The output of the handler gets sent to a storage queue called “invoices”. Then we have a queue storage trigger creating and handling the ProcessInvoiceCommand.

using System.Net.Http;
using System.Threading.Tasks;
using AzureFromTheTrenches.Commanding.Abstractions;
using FluentValidation;
using FunctionMonkey.Abstractions;
using FunctionMonkey.Abstractions.Builders;
using FunctionMonkey.FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FunctionApp2
{
    public class SubmitInvoiceCommand : ICommand<SubmitInvoiceCommand>
    {
        public string Description { get; set; }
        public decimal Amount { get; set; }
    }

    public class SubmitInvoiceCommandValidator : AbstractValidator<SubmitInvoiceCommand>
    {
        public SubmitInvoiceCommandValidator()
        {
            RuleFor(x => x.Description).NotEmpty();
            RuleFor(x => x.Amount).GreaterThan(0);
        }
    }

    public class SubmitInvoiceCommandHandler : ICommandHandler<SubmitInvoiceCommand, SubmitInvoiceCommand>
    {
        public Task<SubmitInvoiceCommand> ExecuteAsync(SubmitInvoiceCommand command, SubmitInvoiceCommand previousResult)
        {
            // We are not actually "handling" anything here, the handler is just returning the sam command
            return Task.FromResult(command);
        }
    }

    public class ProcessInvoiceCommand : ICommand
    {
        public string Description { get; set; }
        public decimal Amount { get; set; }
    }

    public class ProcessInvoiceCommandHandler : ICommandHandler<ProcessInvoiceCommand>
    {
        private readonly ILogger Log;

        public ProcessInvoiceCommandHandler(ILogger log)
        {
            Log = log;
        }

        public Task ExecuteAsync(ProcessInvoiceCommand command)
        {
            Log.LogInformation($"Processing invoice {command.Description} {command.Amount}");
            return Task.CompletedTask;
        }
    }

    public class FunctionAppConfiguration : IFunctionAppConfiguration
    {
        public void Build(IFunctionHostBuilder builder)
        {
            builder
                .Setup((serviceCollection, commandRegistry) =>
                {
                    serviceCollection.AddTransient<IValidator<SubmitInvoiceCommand>, SubmitInvoiceCommandValidator>();
                    commandRegistry.Register<SubmitInvoiceCommandHandler>();
                    commandRegistry.Register<ProcessInvoiceCommandHandler>();
                })
                .AddFluentValidation()
                .Functions(functions => functions

                    .HttpRoute("v1/SubmitInvoice", route => route
                        .HttpFunction<SubmitInvoiceCommand>(HttpMethod.Post)
                        .OutputTo.StorageQueue("invoices"))

                    .Storage(storage => storage
                        .QueueFunction<ProcessInvoiceCommand>("invoices"))                    
                );
        }
    }
}

If we POST the JSON { "Description" : "NAS",    "Amount" : 1000 } we get the following (abridged) output:

Executing HTTP request: {"method": "POST",  "uri": "/api/v1/SubmitInvoice"}
Executing 'SubmitInvoice' 
Executed 'SubmitInvoice'
Executing 'StqFnProcessInvoice' (Reason='New queue message detected on 'invoices')
Storage queue trigger function StqFnProcessInvoice processed a request.
Processing invoice NAS 1000.0
Executed 'StqFnProcessInvoice' 

At the moment the SubmitInvoiceCommandHandler is not doing anything useful, it’s just passing the command back out so it can be output to queue storage.

With Function Monkey you can do away with the command handler in these cases.

One way to do this is when configuring the function app by adding the NoCommandHandler() option in the build method. This means that the SubmitInvoiceCommandHandler class can be deleted:

using System.Net.Http;
using System.Threading.Tasks;
using AzureFromTheTrenches.Commanding.Abstractions;
using FluentValidation;
using FunctionMonkey.Abstractions;
using FunctionMonkey.Abstractions.Builders;
using FunctionMonkey.FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FunctionApp2
{
    public class SubmitInvoiceCommand : ICommand<SubmitInvoiceCommand>
    {
        public string Description { get; set; }
        public decimal Amount { get; set; }
    }

    public class SubmitInvoiceCommandValidator : AbstractValidator<SubmitInvoiceCommand>
    {
        public SubmitInvoiceCommandValidator()
        {
            RuleFor(x => x.Description).NotEmpty();
            RuleFor(x => x.Amount).GreaterThan(0);
        }
    }

    public class ProcessInvoiceCommand : ICommand
    {
        public string Description { get; set; }
        public decimal Amount { get; set; }
    }

    public class ProcessInvoiceCommandHandler : ICommandHandler<ProcessInvoiceCommand>
    {
        private readonly ILogger Log;

        public ProcessInvoiceCommandHandler(ILogger log)
        {
            Log = log;
        }

        public Task ExecuteAsync(ProcessInvoiceCommand command)
        {
            Log.LogInformation($"Processing invoice {command.Description} {command.Amount}");
            return Task.CompletedTask;
        }
    }

    public class FunctionAppConfiguration : IFunctionAppConfiguration
    {
        public void Build(IFunctionHostBuilder builder)
        {
            builder
                .Setup((serviceCollection, commandRegistry) =>
                {
                    serviceCollection.AddTransient<IValidator<SubmitInvoiceCommand>, SubmitInvoiceCommandValidator>();
                    commandRegistry.Register<ProcessInvoiceCommandHandler>();
                })
                .AddFluentValidation()
                .Functions(functions => functions

                    .HttpRoute("v1/SubmitInvoice", route => route
                        .HttpFunction<SubmitInvoiceCommand>(HttpMethod.Post)
                        .Options(options => options.NoCommandHandler())
                        .OutputTo.StorageQueue("invoices"))

                    .Storage(storage => storage
                        .QueueFunction<ProcessInvoiceCommand>("invoices"))                    
                );
        }
    }
}

If we submit the same JSON request we get:

Executing HTTP request: {  "method": "POST",  "uri": "/api/v1/SubmitInvoice"}
Executing 'SubmitInvoice' 
Executed 'SubmitInvoice' 
Executing 'StqFnProcessInvoice' 
Storage queue trigger function StqFnProcessInvoice processed a request.
Processing invoice NAS 1000.0
Executed 'StqFnProcessInvoice'

Even though we don’t have an explicit handler now for the SubmitInvoiceCommand, the validation still takes place.

Another option is to implement the marker interface ICommandWithNoHandler in the command and then you don’t need the .NoCommandHandler() option.

Other Function Monkey articles:

SHARE:

Writing Azure Functions with Function Monkey: Validation

Function Monkey is a framework to define Azure Functions in a fluent way as opposed to using binding attributes on function methods.

Other Function Monkey articles:

In addition to offering a different way to define functions, Function Monkey offers features such as validation.

Consider the following setup that generates a greeting:

using System.Net.Http;
using System.Threading.Tasks;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions;
using FunctionMonkey.Abstractions.Builders;

namespace FunctionApp2
{
    public class GenerateGreetingCommand : ICommand<string>
    {
        public string Name { get; set; }
    }

    public class GenerateGreetingHandler : ICommandHandler<GenerateGreetingCommand, string>
    {
        public Task<string> ExecuteAsync(GenerateGreetingCommand command, string previousResult) => Task.FromResult($"Hello {command.Name}");
    }

    public class FunctionAppConfiguration : IFunctionAppConfiguration
    {
        public void Build(IFunctionHostBuilder builder)
        {
            builder
                .Setup((serviceCollection, commandRegistry) =>
                {
                    commandRegistry.Register<GenerateGreetingHandler>();                    
                })
                .Functions(functions => functions
                    .HttpRoute("v1/GenerateGreeting", route => route
                        .HttpFunction<GenerateGreetingCommand>(HttpMethod.Get))
                );
        }
    }  
}

If we run this and send a JSON payload of {"Name": ""} we’ll get back a response of "Hello ".

There is currently no validation on the name in the GenerateGreetingCommand.

To add validation with Function Monkey install the additional package: FunctionMonkey.FluentValidation. This will also install the dependent package FluentValidation.

To add validation to the Name property, we create a new class that inherits from AbstractValidator<T> where T is the command we want to validate, in this case the GenerateGreetingCommand:

public class GenerateGreetingCommandValidator : AbstractValidator<GenerateGreetingCommand>
{
    public GenerateGreetingCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
    }
}

In the constructor we use the FluentValidation syntax to define what validation to perform on the Name property of the command. In the preceding code we are saying name cannot be empty.

Next we need to wire up this new validator by adding the call to AddFluentValidation() and also registering the validator with serviceCollection.AddTransient<IValidator<GenerateGreetingCommand>, GenerateGreetingCommandValidator>();

So the setup now looks like:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                serviceCollection.AddTransient<IValidator<GenerateGreetingCommand>, GenerateGreetingCommandValidator>();
                commandRegistry.Register<GenerateGreetingHandler>();                    
            })
            .AddFluentValidation()
            .Functions(functions => functions
                .HttpRoute("v1/GenerateGreeting", route => route
                    .HttpFunction<GenerateGreetingCommand>(HttpMethod.Get))
            );
    }
}

If we run the app again and try and submit an empty name, this time we get the following response:

{
  "errors": [
    {
      "severity": 0,
      "errorCode": "NotEmptyValidator",
      "property": "Name",
      "message": "'Name' must not be empty."
    }
  ],
  "isValid": false
}

If we wanted to enforce minimum and maximum Name length:

public class GenerateGreetingCommandValidator : AbstractValidator<GenerateGreetingCommand>
{
    public GenerateGreetingCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty()
                            .MinimumLength(5)
                            .MaximumLength(10);
    }
}

Now if we try and submit a name of “Joe”:

{
  "errors": [
    {
      "severity": 0,
      "errorCode": "MinimumLengthValidator",
      "property": "Name",
      "message": "The length of 'Name' must be at least 5 characters. You entered 3 characters."
    }
  ],
  "isValid": false
}

To add some custom validation in the form of an Action:

public class GenerateGreetingCommandValidator : AbstractValidator<GenerateGreetingCommand>
{
    public GenerateGreetingCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty()
                            .MinimumLength(5)
                            .MaximumLength(10)
                            .Custom((name, context) =>
                                {
                                    if (name == "Jason")
                                    {
                                        context.AddFailure("Jason is not a valid name");
                                    }
                                });

    }
}

Submitting a name of “Jason” now results in:

{
  "errors": [
    {
      "severity": 0,
      "errorCode": null,
      "property": "Name",
      "message": "Jason is not a valid name"
    }
  ],
  "isValid": false
}

We could also go and write units tests for the validator.

The ability to define command validation could also be useful if you had multiple ways for a client to submit requests, for example the same command (and validation) could be triggered from HTTP and a queue for example. In this case you could ensure the same validation is executed regardless of the input “channel”.

SHARE:

Writing Azure Functions with Function Monkey: Dependency Injection

In my continued exploration/experimentation with Function Monkey I thought I’d look at how easy/hard it is to inject dependencies into handlers.

Previous articles: Creating Azure Functions with Function Monkey–First Look and Refactoring an Azure Functions App to use Function Monkey.

If you’ve read the previous articles you’ll know the Function Monkey uses the concept of a command to represent “something that needs doing” and a command handler to “do the thing that needs doing”.

An Azure Function trigger results in the creation of a command, that command is passed to a handler, and the handler can return a result to the caller or an output binding.

Good practice dictates good separation of concerns, etc. so you may want to inject dependencies into your handlers to also make them easier to test.

Let’s start off my defining a dependency to represent the generation of a greeting:

public interface IGreetingGenerator
{
    string GenerateGreeting();
}

And we’ll create a basic implementation:

public class TimeOfDayGreetingGenerator : IGreetingGenerator
{
    public string GenerateGreeting()
    {
        var isAfternoon = DateTime.Now.Hour >= 12;

        if (isAfternoon)
        {
            return "Good afternoon";
        }

        return "Good morning";
    }
}

We could now go and write unit tests for this TimeOfDayGreetingGenerator – however we first have to go and provide a way to deterministically provide a specific date and time.

We’ll create another abstraction represent time so the code becomes:

public interface IGreetingGenerator
{
    string GenerateGreeting();
}

public interface ITime
{
    DateTime Now { get; }
}

public class Time : ITime
{
    public DateTime Now => DateTime.Now;
}

public class TimeOfDayGreetingGenerator : IGreetingGenerator
{
    private readonly ITime Time;

    public TimeOfDayGreetingGenerator(ITime time)
    {
        Time = time;
    }

    public string GenerateGreeting()
    {
        var isAfternoon = Time.Now.Hour >= 12;

        if (isAfternoon)
        {
            return "Good afternoon";
        }

        return "Good morning";
    }
}

And some example tests we could write:

public class TimeOfDayGreetingGeneratorShould
{
    [Fact]        
    public void GenerateMorningGreeting()
    {
        var mockTime = new Mock<ITime>();
        mockTime.Setup(x => x.Now).Returns(new DateTime(2020, 1, 1, 11, 59, 59));
        var sut = new TimeOfDayGreetingGenerator(mockTime.Object);

        var greeting = sut.GenerateGreeting();

        Assert.Equal("Good morning", greeting);
    }

    [Fact]
    public void GenerateAfternoonGreeting()
    {
        var mockTime = new Mock<ITime>();
        mockTime.Setup(x => x.Now).Returns(new DateTime(2020, 1, 1, 13, 0, 0));
        var sut = new TimeOfDayGreetingGenerator(mockTime.Object);

        var greeting = sut.GenerateGreeting();

        Assert.Equal("Good afternoon", greeting);
    }
}

The above tests are using the xUnit.net testing framework and Moq: you can learn how to use both of these by following this Pluralsight skills path that features some of my courses. You can start watching with a free trial.

Next we’ll create a command to represent the requirement to create a greeting for a person:

public class GenerateGreetingCommand : ICommand<string>
{
    public string Name { get; set; }
}

We can now create a handler for this command that also takes an IGreetingGenerator as a constructor dependency:

public class GenerateGreetingHandler : ICommandHandler<GenerateGreetingCommand, string>
{
    private readonly IGreetingGenerator GreetingGenerator;

    public GenerateGreetingHandler(IGreetingGenerator greetingGenerator)
    {
        GreetingGenerator = greetingGenerator;
    }
    public Task<string> ExecuteAsync(GenerateGreetingCommand command, string previousResult)
    {
        return Task.FromResult($"{GreetingGenerator.GenerateGreeting()} {command.Name}");
    }
}

And we can add a test:

public class GenerateGreetingHandlerShould
{
    [Fact]
    public async Task GenerateGreetingWithName()
    {
        var mockGenerator = new Mock<IGreetingGenerator>();
        mockGenerator.Setup(x => x.GenerateGreeting()).Returns("mock greeting");
        var sut = new GenerateGreetingHandler(mockGenerator.Object);
        var command = new GenerateGreetingCommand { Name = "Amrit" };

        var greeting = await sut.ExecuteAsync(command, null);

        Assert.Equal("mock greeting Amrit", greeting);
    }
}

Now we have tested some of the moving parts we can put them all together with Function Monkey (note there are more tests cases we should write but we’ll keep this example short):

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<GenerateGreetingHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/GenerateGreeting", route => route
                    .HttpFunction<GenerateGreetingCommand>(HttpMethod.Get))
            );
    }
}

If we try and run this and submit an HTTP request to the function we’ll get the following error:

Error occurred executing command GenerateGreetingCommand
AzureFromTheTrenches.Commanding: Error occurred during command execution. Microsoft.Extensions.DependencyInjection: Unable to resolve service for type 'FunctionApp2.IGreetingGenerator' while attempting to activate 'FunctionApp2.GenerateGreetingHandler'.

This is because we haven’t wired up the dependencies which we can do by adding:

serviceCollection.AddTransient<ITime, Time>();
serviceCollection.AddTransient<IGreetingGenerator, TimeOfDayGreetingGenerator>();

This makes the entire setup look like the following:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                serviceCollection.AddTransient<ITime, Time>();
                serviceCollection.AddTransient<IGreetingGenerator, TimeOfDayGreetingGenerator>();
                commandRegistry.Register<GenerateGreetingHandler>();                    
            })
            .Functions(functions => functions
                .HttpRoute("v1/GenerateGreeting", route => route
                    .HttpFunction<GenerateGreetingCommand>(HttpMethod.Get))
            );
    }
}

Running the app now and executing the function with the JSON “{"Name": "Sarah"}” returns "Good afternoon Sarah".

It’s nice that DI is built into Function Monkey and that the registration of dependencies is pretty simple.

SHARE:

Refactoring an Azure Functions App to use Function Monkey

In a previous post I took a first look at the Function Monkey library to define Azure Functions using commands and handlers.

In this post I’m going to try and take an existing functions app and convert it to the Function Monkey approach. I should note up-front that this post is not a criticism of the library itself, like everything it’s a work in progress, I may be misunderstanding some of the features :)

The Starting App

The non-function-monkey app creates the workflow: client—>HTTP function –> queue function –> blob function and looks like the following:

using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace FunctionApp2
{
    public class WordsAdditionRequest
    {
        public IEnumerable<string> Words { get; set; }
    }

    public static class Function1
    {
        [FunctionName("AddWords")]
        public static async Task AddWords(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest request,
            [Queue("WordsToProcess")] IAsyncCollector<string> wordQueue,
            ILogger log)
        {
            // validation/error checking logic omitted for brevity

            log.LogInformation("C# HTTP trigger function processed a request.");

            string requestBody = await new StreamReader(request.Body).ReadToEndAsync();
            WordsAdditionRequest wordsAdditionRequest= JsonConvert.DeserializeObject<WordsAdditionRequest>(requestBody);

            foreach (var word in wordsAdditionRequest.Words)
            {
                log.LogInformation($"Adding word '{word}'");
                await wordQueue.AddAsync(word);
            }
        }

        [FunctionName("ProcessWord")]
        public static void ProcessWord(
            [QueueTrigger("WordsToProcess")] string wordToProcess,
            [Blob("uppercase-words/{rand-guid}")] out string uppercaseWord,
            ILogger log)
        {
            log.LogInformation($"C# Queue trigger for word '{wordToProcess}'");

            uppercaseWord = wordToProcess.ToUpperInvariant();
        }

        [FunctionName("AuditWordWritten")]
        public static void AuditWordWritten(
            [BlobTrigger("uppercase-words/{name}")] string uppercaseWord,
            string name,
            ILogger log)
        {
            log.LogInformation($"C# blob trigger - audit for word '{uppercaseWord}'");
        }
    }
}

Refactoring to Function Monkey

First install the NuGets: FunctionMonkey and FunctionMonkey.Compiler.

The next first step is to create a command, so we’ll change WordsAdditionRequest to:

public class AddWordsCommand: ICommand<int>
{
    public IEnumerable<string> Words { get; set; }
}

So far so good, next we’ll need to create a handler for this command. I need the handler to process the command and return each of the strings so they can be added as separate messages to the queue, so we can fan-out the work:

internal class AddWordsHandler : ICommandHandler<AddWordsCommand, string[]>
{
    public Task<string[]> ExecuteAsync(AddWordsCommand command, string[] previousResult)
    {
        return Task.FromResult(command.Words.ToArray());
    }
}

The next thing is to create the configuration class and wire up the hander as a HTTP-triggered function. The initial attempt looks like this:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<AddWordsHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/AddWords", route => route
                    .HttpFunction<AddWordsCommand>(HttpMethod.Post)
                    .OutputTo.StorageQueue("WordsToProcess")
                )
            );
    }
}

 

Now we have an HTTP function outputting to a storage queue.

If we run this and submit a HTTP request containing the JSON { "words": ["apple", "pear"]} we get 2 messages added to the WordsToProcess queue.

Messages in the queue

What’s nice here is that because the command and handler return string[] Function Monkey has automatically “fanned-out” the data into multiple messages.When I was writing the code I was looking for specific ways to implement this which wasted some time. Really this is a nice intuitive way of handling IEnumerable return values.

So far so good. The next step is to read the messages from the queue, convert them to upper case, and then write out the blobs. This is unfortunately where I ran into some roadblocks.

First I defined command/hander:

public class ConvertToUpperCaseCommand : ICommand<string>
{
    public string Word { get; set; }
}

internal class ConvertToUpperCaseHandler : ICommandHandler<ConvertToUpperCaseCommand, string>
{        
    public Task<string> ExecuteAsync(ConvertToUpperCaseCommand command, string previousResult)
    {
        return Task.FromResult(command.Word.ToUpperInvariant());
    }
}

Then I tried to modify the config:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<AddWordsHandler>();
                commandRegistry.Register<ConvertToUpperCaseHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/AddWords", route => route
                    .HttpFunction<AddWordsCommand>(HttpMethod.Post)
                    .OutputTo.StorageQueue("WordsToProcess")                    
                )
            .Storage(storageFunctions => storageFunctions
                .QueueFunction<ConvertToUpperCaseCommand>("WordsToProcess")
                .OutputTo.StorageBlob // StorageBlob does not exist
            );
    }
}

This is where I ran into a problem, I couldn't find an option to output to blob storage:

Visual Studio screenshot showing no method to output to blob storage

There are output bindings for storage queues and tables but not for blobs. I could of course be misunderstanding how to configure this.

To get around this I’m going to try and mix Function Monkey with traditional attributes by adding the following:

[FunctionName("ProcessWord")]
public static void ProcessWord(
    [QueueTrigger("WordsToProcess")] string wordToProcess,
    [Blob("uppercase-words/{rand-guid}")] out string uppercaseWord,
    ILogger log)
{
    log.LogInformation($"C# Queue trigger for word '{wordToProcess}'");

    uppercaseWord = wordToProcess.ToUpperInvariant();
}

Now running the app and making an HTTP request results in the messages being added to the queue via Function Monkey and then the traditionally-specified ProcessWord function executes and writes to blob storage.

It’s nice that you can mix Function Monkey with the attribute-based function definition, though I don’t know if this is recommended or not and whether not there could by any unintentional side-effects.

The final part is the blob-triggered audit function.

Once again I’ll define a command and handler:

public class AuditCommand : ICommand, IStreamCommand
{
    public Stream Stream { get; set; }

    public string Name { get; set; }
}
internal class AuditHandler : ICommandHandler<AuditCommand>
{
    public Task ExecuteAsync(AuditCommand command)
    {           
        using (StreamReader reader = new StreamReader(command.Stream))
        {
            string name = reader.ReadToEnd();

            // How to log name?
        }

        return Task.CompletedTask;
    }
}

The version of a blob-triggered command allows us to get a Stream representing the blob data (simpler blob commands without streams are also available).

We can now wire this up:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<AddWordsHandler>();
                commandRegistry.Register<AuditHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/AddWords", route => route
                    .HttpFunction<AddWordsCommand>(HttpMethod.Post)
                    .OutputTo.StorageQueue("WordsToProcess"))
                .Storage(storageFunctions => storageFunctions
                .BlobFunction<AuditCommand>("uppercase-words/{name}"))
            );
    }
}

Running this now results in the blob being written and the audit command handler executing. The original hander just output the blob to the log.

I’m not sure how to get access to the log in a handler, so I’m going to just add a constructor that takes an ILogger and see what happens:

internal class AuditHandler : ICommandHandler<AuditCommand>
{
    private readonly ILogger log;

    public AuditHandler(ILogger log)
    {
        this.log = log;
    }
    public Task ExecuteAsync(AuditCommand command)
    {           
        using (StreamReader reader = new StreamReader(command.Stream))
        {
            string name = reader.ReadToEnd();

            log.LogInformation($"C# blob trigger - audit for word '{name}'");
        }

        return Task.CompletedTask;
    }
}

Running this and it just works, the ILogger is injected into the handler and the log message is output: C# blob trigger - audit for word 'APPLE'

There’s a lot more to Function Monkey by the looks of it such as DI, validation, etc.

The final code:

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions;
using FunctionMonkey.Abstractions.Builders;
using FunctionMonkey.Commanding.Abstractions;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;

namespace FunctionApp2
{
    public class AddWordsCommand : ICommand<string[]>
    {
        public IEnumerable<string> Words { get; set; }
    }

    internal class AddWordsHandler : ICommandHandler<AddWordsCommand, string[]>
    {
        public Task<string[]> ExecuteAsync(AddWordsCommand command, string[] previousResult)
        {
            return Task.FromResult(command.Words.ToArray());
        }
    }

    public class AuditCommand : ICommand, IStreamCommand
    {
        public Stream Stream { get; set; }

        public string Name { get; set; }
    }

    internal class AuditHandler : ICommandHandler<AuditCommand>
    {
        private readonly ILogger log;

        public AuditHandler(ILogger log)
        {
            this.log = log;
        }
        public Task ExecuteAsync(AuditCommand command)
        {           
            using (StreamReader reader = new StreamReader(command.Stream))
            {
                string name = reader.ReadToEnd();

                log.LogInformation($"C# blob trigger - audit for word '{name}'");
            }

            return Task.CompletedTask;
        }
    }

    public class FunctionAppConfiguration : IFunctionAppConfiguration
    {
        public void Build(IFunctionHostBuilder builder)
        {
            builder
                .Setup((serviceCollection, commandRegistry) =>
                {
                    commandRegistry.Register<AddWordsHandler>();
                    commandRegistry.Register<AuditHandler>();
                })
                .Functions(functions => functions
                    .HttpRoute("v1/AddWords", route => route
                        .HttpFunction<AddWordsCommand>(HttpMethod.Post)
                        .OutputTo.StorageQueue("WordsToProcess"))
                    .Storage(storageFunctions => storageFunctions
                    .BlobFunction<AuditCommand>("uppercase-words/{name}"))
                );
        }
    }

    public static class Function1
    {
        [FunctionName("ProcessWord")]
        public static void ProcessWord(
            [QueueTrigger("WordsToProcess")] string wordToProcess,
            [Blob("uppercase-words/{rand-guid}")] out string uppercaseWord,
            ILogger log)
        {
            log.LogInformation($"C# Queue trigger for word '{wordToProcess}'");

            uppercaseWord = wordToProcess.ToUpperInvariant();
        }
    }   
}

SHARE:

Creating Azure Functions with Function Monkey–First Look

I’ve had Function Monkey on my to-look-at radar for a little while now so I thought I’d finally get round to looking at it in this post.

As a writing experiment I’m going to “live write” my experience of using it for the first time.

Function Monkey is essentially a tool/library for building an Azure Functions app in a different way from the usual function-methods-with-attributes.

Let’s go!

Setting Up A Project

So the first thing I need to do is create a new Azure Functions project in  Visual Studio – I’m choosing to create an empty functions app.

Next, I’m installing the NuGet packages: FunctionMonkey and FunctionMonkey.Compiler

Defining a Command

Function Monkey uses commands to represent “things the functions can do”.

For example to create a command that represent the addition of 2 numbers I’m adding the following:

using AzureFromTheTrenches.Commanding.Abstractions;

namespace FunctionApp1
{
    public class AddNumbers : ICommand<int>
    {
        public int FirstNumber { get; set; }
        public int SecondNumber { get; set; }
    }
}

The ICommand<int> says that when this command executes it will return an int.

Handling a Command

Now I’ve defined a command, I need to be able to execute it so I need to create a “command handler”, so I’m adding a  new class:

internal class AddNumbersHandler : ICommandHandler<AddNumbers, int>
{
    public Task<int> ExecuteAsync(AddNumbers command, int previousResult)
    {
        return Task.FromResult(command.FirstNumber + command.SecondNumber);
    }
}

I now need a wire to make the handler execute.

Function App Configuration

In function apps you normally create a class and decorate methods with attributes to create functions. With Function Monkey it looks like there’s a fluent API instead, so I’m adding a new class to wire everything up:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<AddNumbersHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/AddNumbers", route => route
                    .HttpFunction<AddNumbers>()
                )
            );
    }
}

This code is wiring up the command and handler that we just created and setting up an HTTP-triggered function.

I’m not sure at first glance how I feel about the readability of this as I’m looking at it, though it is an interesting way to wire things up, I believe you can also wire up dependency injection and other things here which would be nice.

Testing It Out

Ok, let’s run the app and see what happens:

Application started. Press Ctrl+C to shut down.

Http Functions:

        AddNumbers: [GET] http://localhost:7071/api/v1/AddNumbers

[31/01/2020 4:49:07 AM] Host lock lease acquired by instance ID '0000000000000000000000006B155899'.

So it looks like the HTTP-triggered function has been created in the functions runtime with the route “v1/AddNumbers”.

It’s not immediately obvious what HTTP request to make, it’s exposing a GET so let’s try and make a GET request with some JSON to represent the command properties FirstNumber and SecondNumber.

This is the JSON I’m going to try (I’m going to use the Postman app to send the request):

{
    "FirstNumber" : 1,
    "SecondNumber" : 1
}

Sending this JSON results in the value 2 being returned so this part where JSON maps to the command properties is nice and intuitive.

There’s also probably an easy way to restrict this API to POSTs. Without looking at the docs I’m going to see if it’s easy to find/intuitive to modify the code where the function is defined…

So the HTTP method has an overload that takes an params array of HttpMethod, so I just changed the config to:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup((serviceCollection, commandRegistry) =>
            {
                commandRegistry.Register<AddNumbersHandler>();
            })
            .Functions(functions => functions
                .HttpRoute("v1/AddNumbers", route => route
                    .HttpFunction<AddNumbers>(HttpMethod.Post)
                )
            );
    }
}

Let’s run the app again:

Application started. Press Ctrl+C to shut down.

Http Functions:

        AddNumbers: [POST] http://localhost:7071/api/v1/AddNumbers

[31/01/2020 7:00:38 AM] Host lock lease acquired by instance ID '0000000000000000000000006B155899'.

Now as we can see the URL accepts POSTS.

This is a library that I think warrants further inspection. Let me know in the comments if you’d like to see more posts on this.

One thing to bear in mind when using these kinds of tools is that they do introduce another dependency to the application.

SHARE:

Refactoring Code to Use C# Local Functions

In a previous post I talked about the potential use of local functions to replace comments. This generated some good discussion on Twitter and in the comments.

In this post I wanted to show another use of local functions to potentially improve readability.

Improving Iteration Code Readability

Consider the following simple console app:

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var names = new[]{ "Sarah Smith", "Gentry Jones", "Arnold Appleview" };

            // Output names with surname first
            foreach (var name in names)
            {
                var nameParts = name.Split(" ");
                var firstName = nameParts[0];
                var lastName = nameParts[1];
                var formattedName = $"{lastName}, {firstName}";
                Console.WriteLine(formattedName);
            }

            Console.ReadLine();
        }
    }
}

The code inside the for loop could be considered at a lower level of abstraction/more detailed than the rest of the code and the Main method itself.

We could take the contents of the for loop and refactor it into a private function in the class as follows:

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var names = new[] { "Sarah Smith", "Gentry Jones", "Arnold Appleview" };
            
            foreach (var name in names)
            {
                OutputWithSurnameFirst(name);
            }

            Console.ReadLine();
        }

        private static void OutputWithSurnameFirst(string name)
        {
            var nameParts = name.Split(" ");
            var firstName = nameParts[0];
            var lastName = nameParts[1];
            var formattedName = $"{lastName}, {firstName}";
            Console.WriteLine(formattedName);
        }
    }
}

Notice the Main method is not mixing as many levels of abstraction/detail now – we have also been able to remove the comment because the method name OutputWithSurnameFirst describes what the comment used to. If we are reading the Main method, we don’t have to burden our concentration with the details of the how the names are output unless we want to.

This approach is fine, but it could be argued that we have “polluted” the class with a method that is only used once in the Main method. It could also be argued that declaration of the OutputWithSurnameFirst method is not as close to it’s use as a local method would be.

Let’s take a look next at a version of the code that instead uses a local method:

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var names = new[] { "Sarah Smith", "Gentry Jones", "Arnold Appleview" };

            foreach (var name in names)
            {
                OutputWithSurnameFirst(name);
            }

            Console.ReadLine();

            static void OutputWithSurnameFirst(string name)
            {
                var nameParts = name.Split(" ");
                var firstName = nameParts[0];
                var lastName = nameParts[1];
                var formattedName = $"{lastName}, {firstName}";
                Console.WriteLine(formattedName);
            }
        }
    }
}

In the preceding example, the local method has taken the place of the class-level method, it has however made the overall length of the Main method longer. In this example it could be argued that the previous version is more readable.

Let’s take a look at another example next.

Improving C# Lambda Code Readability with Local Functions

In some cases, a local function may improve the readability of lambda function code.

Take the following initial code:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var strings = new[] { "Hello", "31414", "2HI9" };

            foreach (var name in strings)
            {
                IEnumerable<bool> areUpperLetters = name.Select(x => char.IsLetter(x) && char.IsUpper(x));
                Console.WriteLine($"{name} upper digits = {string.Join(",", areUpperLetters)}");
            }

            Console.ReadLine();
        }
    }
}

In the preceding code, the variable named areUpperLetters gives us a clue as to what the lambda does, which is good, often a well-named variable can really improve readability. This code could be refactored to a local functions as follows:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var strings = new[] { "Hello", "31414", "2HI9" };

            foreach (var name in strings)
            {
                IEnumerable<bool> areUpperLetters = name.Select(IsUpperCaseLetter);
                Console.WriteLine($"{name} upper digits = {string.Join(",", areUpperLetters)}");
            }

            Console.ReadLine();

            static bool IsUpperCaseLetter(char c)
            {
                return char.IsLetter(c) && char.IsUpper(c);
            }
        }
    }
}

Now the logic that was contained in the lambda has been moved to the local function called IsUpperCaseLetter.

To see local functions in action and also learn a whole heap of C# tips check out my C# Tips and Traps Pluralsight course. You can also start watching the course with a free trial .

SHARE:

Using Local Functions to Replace Comments

One idea I’ve been thinking about recently is the replacement of comments with local function calls.

Now this idea doesn’t mean that it’s ok to have massive functions that have no functional cohesion but instead in some circumstances it may improve readability.

In C#, local functions were introduced in C# 7.0. They essentially allow you to write a function inside of a method, property get/set, etc.

As an example take the following code:

public static void ProcessSensorData(string data)
{
    // HACK: occasionally a sensor hardware glitch adds extraneous $ signs
    data = data.Replace("$", "");

    string upperCaseName = data.ToUpperInvariant();
    Save(upperCaseName);
}

private static void Save(string data)
{
    // Save somewhere etc.
    Console.WriteLine(data);
}

In the preceding code there is a hack to fix broken sensors that keep adding extra $ signs.

This could be written using a local function as follows:

public static void ProcessSensorData(string data)
{
    FixExtraneousSensorData();
    string upperCaseName = data.ToUpperInvariant();
    Save(upperCaseName);

    void FixExtraneousSensorData()
    {
        data = data.Replace("$", "");
    }
}

Notice in this version, there is a local function FixExtraneousSensorData that strips out the $ signs. This function is named to try and convey the comment that we had before: “occasionally a sensor hardware glitch adds extraneous $ signs”. Also notice the local function has direct access to the variables of the method in which they’re declared, in this case data.

There are other options here of course such as creating a normal non-local class-level function and passing data to it, or perhaps creating and injecting a data sanitation class as a dependency.

Replacing Arrange, Act, Assert Comments in Unit Tests

As another example consider the following test code:

[Fact]
public void HaveSanitizedFullName()
{
    // Arrange
    var p = new Person
    {
        FirstName = "    Sarah ",
        LastName = "  Smith   "
    };

    // Act
    var fullName = p.CreateFullSanitizedName();

    // Assert
    Assert.Equal("Sarah Smith", fullName);
}

Notice the comments separating the logical test phases.

Again these comments could be replaced with local functions as follows:

[Fact]
public void HaveSanitizedFullName_LocalFunctions()
{
    Person p;
    string fullName;

    Arrange();
    Act();
    AssertResults();
    
    void Arrange()
    {
        p = new Person
        {
            FirstName = "    Sarah ",
            LastName = "  Smith   "
        };
    }

    void Act()
    {
        fullName = p.CreateFullSanitizedName();
    }

    void AssertResults()
    {
        Assert.Equal("Sarah Smith", fullName);
    }
}

This version is a lot longer and although we’ve rid ourselves of the comments the test body is a lot longer, with more lines of code, and I think is probably not as readable. Obviously the test is very simple, if you’ve got a lot of test arrange code for example you could just abstract the arrange phase perhaps.

Another option in the test code to remove the comments is to make use of the most basic unit of design – white space. So for example we could remove comments and still give a clue to the various phases as follows:

[Fact]
public void HaveSanitizedFullName_WhiteSpace()
{
    var p = new Person
    {
        FirstName = "    Sarah ",
        LastName = "  Smith   "
    };


    
    var fullName = p.CreateFullSanitizedName();

    

    Assert.Equal("Sarah Smith", fullName);
}

I think the tactical use of local functions like in the first example to replace the hack comment  may be more useful than replacing the (arguably extraneous) arrange act assert comments in tests.

Let me know in the comments if you think this is a good idea, a terrible idea, or something that you might use now and again.

SHARE:

New Pluralsight Course: C# Tips and Traps

“Sometimes it's hard to know what you don't know. Short-circuit your learning of C# and level-up your code with this collection of C# and .NET features.” This is the short description from my newest Pluralsight course that just launched called C# Tips and Traps.

As the description suggests, this course is designed to fill in the gaps in your C# knowledge and to highlight ways to improve your code using often little-known or underused features of C# and .NET.

The course is organized thematically into a number of modules but the great thing about it is you can start with any module that interests you and then move on to another one – you could also just watch them sequentially so you don’t miss out on any tips.

The course modules are as follows:

  • String, Character, and Formatting Tips
  • Tips for Working with Numbers and Dates
  • Data Types and Object Tips
  • Tips for Working with Files, Paths, and URIs
  • Organizing and Structuring Classes and Code
  • Compilation Tips
  • Tips for Casting and Conversions
  • Runtime Execution Tips
  • Bonus Tips

At a total of just over 4 hours running time, there is a ton of value in this course and you can even just dip into the next tip whenever you have a spare 5-10 mins, you could even watch one tip/clip per day with breakfast and see if you can use it at work that day!

If you’re looking for a course that you can consume in bite-sized chunks over the coming holidays and fit in a bit of learning around family and holiday activities this is a good candidate.

Hope you enjoy watching it, and don’t forget you can get a Pluralsight free trial to start watching it.

Ps. This course is an update/amalgamation of two older courses: C# Tips and Traps and C# Tips and Traps 2.

SHARE:

Understanding Azure Durable Functions - Part 12: Sub Orchestrations

This is part twelve in a series of articles. If you’re not familiar with Durable Functions you should check out the previous articles before reading this.

Sub-orchestrations are a feature of Durable Functions that allow you to further compose and reuse functions.

Essentially sub-orchestrations allow you to call an orchestration from within another orchestration. In this way they are similar to calling activity functions from within an orchestration and just like activity functions can return a value to the calling (parent) orchestration.

As an example, the following client function starts the orchestration called ProcessMultipleCitiesOrhestrator:

[FunctionName("SubOrchExample_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{

    var data = await req.Content.ReadAsAsync<GreetingsRequest>();

    string instanceId = await starter.StartNewAsync("ProcessMultipleCitiesOrhestrator", data);

    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    return starter.CreateCheckStatusResponse(req, instanceId);
}

The preceding code is no different from what we’ve seen already in this series, the change comes in the ProcessMultipleCitiesOrhestrator:

[FunctionName("ProcessMultipleCitiesOrhestrator")]
public static async Task<string> ParentOrchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context, 
    ILogger log)
{
    log.LogInformation($"************** ProcessMultipleCitiesOrhestrator ********************");

    GreetingsRequest data = context.GetInput<GreetingsRequest>();


    // Perform all greetings in parallel executing sub-orchestrations
    var greetingsSubOrchestrations = new List<Task>();            
    
    foreach (string city in data.Cities)
    {
        Task greetingSubOrchestration = context.CallSubOrchestratorAsync<string>("ProcessSingleCityOrhestrator", city);
        greetingsSubOrchestrations.Add(greetingSubOrchestration);
    }

    await Task.WhenAll(greetingsSubOrchestrations);

    // When all of the sub orchestrations have competed, get the results and append into a single string
    var allGreetings = new StringBuilder();
    foreach (Task<string> greetingSubOrchestration in greetingsSubOrchestrations)
    {
        allGreetings.AppendLine(await greetingSubOrchestration);
    }

    log.LogInformation(allGreetings.ToString());

    return allGreetings.ToString();
}

The main thing to note in the preceding code is the line: Task greetingSubOrchestration = context.CallSubOrchestratorAsync<string>("ProcessSingleCityOrhestrator", city); Here we are calling into another orchestrator function (the sub-orchestration). We are doing this by creating a list of tasks, each representing an instance of the sub-orchestration, and then executing those tasks, and finally getting the return values from each sub-orchestration task and creating a single string result.

The ProcessSingleCityOrhestrator function is as follows:

[FunctionName("ProcessSingleCityOrhestrator")]
public static async Task<string> SubOrchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    log.LogInformation($"************** ProcessSingleCityOrhestrator method executing ********************");

    var city = context.GetInput<string>();

    string greeting = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction", city);
    string toUpper = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction_ToUpper", greeting);
    string withTimestamp = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction_AddTimestamp", toUpper);

    log.LogInformation(withTimestamp);

    return withTimestamp;
}

Now we have the flexibility to compose/reuse, for example the ProcessSingleCityOrhestrator could be called from a different client function if only a single city was being supplied.

If we call the client function SubOrchExample_HttpStart  with the JSON:

{
    "Cities": [
            "London",
            "Tokyo",
            "Perth",
            "Nadi"
    ]
}

We get the following return value (from the ProcessMultipleCitiesOrhestrator):

{
    "name": "ProcessMultipleCitiesOrhestrator",
    "instanceId": "c7a0eb03c56a44ab8a767cd8a487c834",
    "runtimeStatus": "Completed",
    "input": {
        "$type": "DurableDemos.SubOrchExample+GreetingsRequest, DurableDemos",
        "Cities": [
            "London",
            "Tokyo",
            "Perth",
            "Nadi"
        ]
    },
    "customStatus": null,
    "output": "HELLO LONDON! [30/10/2019 11:51:53 AM +08:00]\r\nHELLO TOKYO! [30/10/2019 11:51:58 AM +08:00]\r\nHELLO PERTH! [30/10/2019 11:52:01 AM +08:00]\r\nHELLO NADI! [30/10/2019 11:52:00 AM +08:00]\r\n",
    "createdTime": "2019-10-30T03:51:52Z",
    "lastUpdatedTime": "2019-10-30T03:52:01Z"
}

To learn more about sub-orchestrations, check out the docs.

If you like this article or series feel free to share it :)

SHARE: