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.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

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:

Watch All My Pluralsight Courses For Free This Weekend

This weekend (Friday 22nd to Sunday 24th of November 2019) you can watch all my Pluralsight courses for free.

You could also watch an entire skills path such as C# Unit Testing with xUnit or C# Unit Testing with NUnit.

The free weekend starts November 22nd at 10:00am Mountain Time.

Check this link for the full list of all my courses.

SHARE:

One Simple Technique to Help Achieve Your Goals

As the New Year approaches and people start to comment “I can’t believe it’s November”, thoughts start to turn to New Year’s Resolutions and things not accomplished in this year.

Whilst I don’t believe in New Year’s Resolutions per se, rather I try to adopt a mindset of continuous improvement, the end of the year is a great time for reflection.

At the start of each year, I make use of the Three Wins Technique to think about 3 goals (aka “wins”) for the year.

Whether you like to make New Year’s Resolutions, use the 3 Wins or another technique, there’s one thing that a lot of people don’t seem to do…

…and that’s to write them down.

It sounds silly, “I know what I want to do next year”, but for some reason, somehow, writing down your goals gives them power.

You could write them on sticky notes and put them on your bathroom mirror so you see them every day. You could write them on a bit of paper and keep them in your wallet, purse, handbag, backpack, etc. You could (as I do) have a OneNote page for every year with my 3 Wins listed at the top with check boxes next to them.

The important thing is to write them down.

Since I started writing down what I wanted to achieve, I have achieved more. Maybe not everything, but still more.

I’m not saying “write it down and trust in manifestation”, you’ve still got to do the work, but at least start by writing down what you want to achieve.

If you think this sounds silly, why not give it a go anyway? Take your goals, write them down,and see what happens…

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 want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Learning xUnit .NET Unit Testing the Easy Way

If you’re getting started with .NET or you’ve done some testing but want to know how to put it all together and also learn some additional tools then the new xUnit.net testing path from Pluralsight may be of interest (you can also get started viewing for free with a trial).

The path currently has the following individual courses (including some of my courses) taking you right from the basics of xUnit.net to more advanced techniques:

  • Testing .NET Code with xUnit.net: Getting Started
  • Mocking in .NET Core Unit Tests with Moq: Getting Started
  • Creating Maintainable Contexts for Automated Testing
  • Writing Testable Code
  • Building a Pragmatic Unit Test Suite
  • Improving Unit Tests with Fluent Assertions
  • Better .NET Unit Tests with AutoFixture: Get Started
  • Approval Tests for .NET

If you’re already skilled with xUnit.net you may find some of the other courses in the path useful.

You can start watching with a Pluralsight free trial.

SHARE:

Watch My Pluralsight Courses For Free This Weekend

If you don’t have a Pluralsight subscription and you want to watch my courses you can this weekend for FREE!

Click the ad below to get started and then once you've signed up for your free weekend, head over to MY LIST OF COURSES to get started!

SHARE:

Understanding Azure Durable Functions - Part 11: The Asynchronous Human Interaction Pattern

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

The Asynchronous Human Interaction Pattern allows a Durable Functions orchestration to pause at some point during its lifecycle and wait for an external event such as a human to perform some action or make some decision.

Azure Functions Durable Functions and Twilio working together

As an example, suppose that a comment can be submitted on a website but before actually appearing it must be moderated by a human. The human moderator can see the comment and then decide whether to approve the comment so it appears on the website or decline the comment in which case it is deleted.

In this scenario, if the human moderator does not approve or decline the comment within a set amount of time then the comment will be escalated to a manager to review.

Azure Durable Functions makes this possible because the orchestration can wait for an external event during its execution.

The code in this article follows the following workflow:

  1. New comment submitted via HTTP
  2. Review/moderation orchestration started
  3. Orchestration sends SMS notification to moderator
  4. Moderator receives SMS that contains 2 links: one to approve and one to decline the comment
  5. Orchestration waits for human moderator to click on one of the 2 links
  6. When human clicks a link, the orchestration resumes and comment is approved or declined
  7. If human does not click link within a set deadline, the comment is escalated to a human manager

Let’s start by defining a class to be HTTP POSTed to the client function:

public class AddCommentRequest
{
    public string UserName { get; set; }
    public string Comment { get; set; }
}

And the client function:

[FunctionName("HumanPatternExample_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    var commentRequest = await req.Content.ReadAsAsync<AddCommentRequest>();

    string instanceId = await starter.StartNewAsync("HumanPatternExample_Orchestrator", (commentRequest: commentRequest, requestUri: req.RequestUri));

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

    return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted)
    {
        Content = new StringContent("Your comment has been submitted and is awaiting moderator approval.")
    };
}

The preceding client function initiates the HumanPatternExample_Orchestrator function and passes in a tuple containing the comment request and the request URI which will be used later to construct the approve/decline URL links.

We’ll have a look at this orchestrator function in a moment, but first let’s take a look at the activity function that sends the SMS to the moderator:

[FunctionName("HumanPatternExample_RequestApproval")]
[return: TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%FromPhoneNumber%")]
public static CreateMessageOptions RequestApproval([ActivityTrigger] ModerationRequest moderationRequest, ILogger log)
{            
    log.LogInformation($"Requesting approval for comment: {moderationRequest.CommentRequest.Comment}.");

    // Here we provide some way for a human to know that there is a new comment pending approval.
    // This could be writing to a database representing requests not yet approved for a human
    // to work through, or an SMS message for them to reply to with either APPROVED or NOTAPPROVED
    // or an email for them to reply to etc etc.

    var approversPhoneNumber = new PhoneNumber(Environment.GetEnvironmentVariable("ApproverPhoneNumber", EnvironmentVariableTarget.Process));                        

    var message = new CreateMessageOptions(approversPhoneNumber)
    {
        Body = $"'{moderationRequest.CommentRequest.Comment}' \r\nApprove: {moderationRequest.ApproveRequestUrl} \r\nDecline: {moderationRequest.DeclineRequestUrl}"
    };

    log.LogInformation($"Sending SMS: {message.Body}");

    return message;
}

In the preceding code, the TwilioSms output binding is being used to send an SMS – the SMS will contain links to either approve or decline the comment as the following screenshot shows:

Azure Functions and Twilio integration

Also notice that the ActivityTrigger is bound to a ModerationRequest: public static CreateMessageOptions RequestApproval([ActivityTrigger] ModerationRequest moderationRequest, ILogger log) – this class is defined as follows:

public class ModerationRequest
{
    public AddCommentRequest CommentRequest { get; set; }
    public string ApproveRequestUrl { get; set; }
    public string DeclineRequestUrl { get; set; }
}

The orchestrator function is where the main workflow is defined:

[FunctionName("HumanPatternExample_Orchestrator")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    log.LogInformation($"************** RunOrchestrator method executing ********************");

    // Using tuples but could also define a class for this data
    var (commentRequest, requestUri) = context.GetInput<Tuple<AddCommentRequest,Uri>>();
    var moderationRequest = new ModerationRequest
    {
        CommentRequest = commentRequest,
        ApproveRequestUrl = $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}/api/HumanPatternExample_Approve?id={context.InstanceId}",
        DeclineRequestUrl = $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}/api/HumanPatternExample_Decline?id={context.InstanceId}",
    };
    await context.CallActivityAsync("HumanPatternExample_RequestApproval", moderationRequest);

    // Define a time out - if the moderator hasn't approved/decline then escalate to someone else, e.g. a manager
    using (var timeout = new CancellationTokenSource())
    {
        DateTime moderationDeadline = context.CurrentUtcDateTime.AddMinutes(5); // probably would be longer in real life

        Task durableTimeout = context.CreateTimer(moderationDeadline, timeout.Token);

        Task<bool> moderatedEvent = context.WaitForExternalEvent<bool>("Moderation");

        if (moderatedEvent == await Task.WhenAny(moderatedEvent, durableTimeout))
        {
            timeout.Cancel();

            bool isApproved = moderatedEvent.Result;

            if (isApproved)
            {
                log.LogInformation($"************** Comment '{commentRequest.Comment}' was approved by a moderator ********************");
                // call an activity to make the comment live on the website, etc.
            }
            else
            {
                log.LogInformation($"************** Comment '{commentRequest.Comment}' was declined by a moderator ********************");
                // call an activity to delete the comment and don't make it live on website, etc.
            }
        }
        else
        {
            log.LogInformation($"************** Comment '{commentRequest.Comment}' was not reviewed by a moderator in time, escalating...  ********************");
            // await context.CallActivityAsync("Escalate"); call an activity to escalate etc.
        }
    }

    log.LogInformation($"************** Orchestration complete ********************");
}

The code may look a little complex at first, let’s break it down into the more important parts as they relate to the Asynchronous Human Interaction Pattern:

The line await context.CallActivityAsync("HumanPatternExample_RequestApproval", moderationRequest); calls the activity that actually notifies the human in some way that the orchestration is waiting for them.

Two tasks are created: Task durableTimeout = context.CreateTimer(moderationDeadline, timeout.Token); and Task<bool> moderatedEvent = context.WaitForExternalEvent<bool>("Moderation"); The first task represents the deadline/timeout that the moderator has. The second uses the DurableOrchestrationContext.WaitForExternalEvent method to pause the orchestration until an event occurs outside of the orchestration (i.e. the human interaction). Once these 2 tasks are defined, the line if (moderatedEvent == await Task.WhenAny(moderatedEvent, durableTimeout)) checks to see if the orchestration is continuing because of an external event or because of the timeout .

So if the orchestration is waiting for an external event, how is that event sent to the orchestration? This is done via the DurableOrchestrationClient.RaiseEventAsync method as the following code shows:

[FunctionName("HumanPatternExample_Approve")]
public static async Task<IActionResult> HumanPatternExample_Approve(
    [HttpTrigger(AuthorizationLevel.Function, "get")]HttpRequest req,
    [OrchestrationClient]DurableOrchestrationClient client,
    ILogger log)
{
    // additional validation/null check code omitted for brevity

    var id = req.Query["id"];

    var status = await client.GetStatusAsync(id);

    if (status.RuntimeStatus == OrchestrationRuntimeStatus.Running)
    {
        await client.RaiseEventAsync(id, "Moderation", true);
        return new OkObjectResult("Comment was approved.");
    }

    return new NotFoundResult();
}

[FunctionName("HumanPatternExample_Decline")]
public static async Task<IActionResult> HumanPatternExample_Decline(
    [HttpTrigger(AuthorizationLevel.Function, "get")]HttpRequest req,
    [OrchestrationClient]DurableOrchestrationClient client,
    ILogger log)
{
    // additional validation/null check code omitted for brevity

    var id = req.Query["id"];

    var status = await client.GetStatusAsync(id);
    if (status.RuntimeStatus == OrchestrationRuntimeStatus.Running)
    {
        await client.RaiseEventAsync(id, "Moderation", false);
        return new OkObjectResult("Comment was declined.");
    }

    return new NotFoundResult();
}

The preceding 2 functions are triggered via HTTP (the links that are sent in the SMS) and raise the “Moderation” event to the orchestration id with either true or false.

The orchestrator is waiting for this event: Task<bool> moderatedEvent = context.WaitForExternalEvent<bool>("Moderation"); If the event is received in the orchestrator,  the approval decision is determined: bool isApproved = moderatedEvent.Result; The sample code then uses an if statement to either publish the comment to the website or delete it (omitted for brevity).

Let’s take a look at some (simplified) output – first if the human moderator approves the comment:

Executing HTTP request: {
  "requestId": "c300dfdb-553a-4d1a-8685-7eec6e9fc375",
  "method": "POST",
  "uri": "/api/HumanPatternExample_HttpStart"
}
Executing 'HumanPatternExample_HttpStart' (Reason='This function was programmatically called via the host APIs.', Id=2ff016f6-d54e-4489-9497-393322165d24)
Started orchestration with ID = 'c5ea86d46e524641a49ca10d1c04efc5'.
Executing 'HumanPatternExample_Orchestrator' (Reason='', Id=43196ec8-83cd-4a82-9c63-fcc9f13bd114)
************** RunOrchestrator method executing ********************
Executing 'HumanPatternExample_RequestApproval' (Reason='', Id=c98b259c-100a-4730-a1a9-116f5ea11aa1)
Requesting approval for comment: I hate cheese.
Sending SMS: 'I hate cheese'
Approve: http://localhost:7071/api/HumanPatternExample_Approve?id=c5ea86d46e524641a49ca10d1c04efc5
Decline: http://localhost:7071/api/HumanPatternExample_Decline?id=c5ea86d46e524641a49ca10d1c04efc5
Executed 'HumanPatternExample_RequestApproval' (Succeeded, Id=c98b259c-100a-4730-a1a9-116f5ea11aa1)
'HumanPatternExample_Orchestrator (Orchestrator)' is waiting for input. Reason: CreateTimer
'HumanPatternExample_Orchestrator (Orchestrator)' is waiting for input. Reason: WaitForExternalEvent:Moderation

<I 'clicked' the approve link here>

Executing 'HumanPatternExample_Approve' 
Function 'HumanPatternExample_Orchestrator (Orchestrator)' scheduled. Reason: RaiseEvent:Moderation
Function 'HumanPatternExample_Orchestrator (Orchestrator)' received a 'Moderation' event
************** Comment 'I hate cheese' was approved by a moderator ********************
************** Orchestration complete ********************

If we submit another request and wait or 5 minutes (DateTime moderationDeadline = context.CurrentUtcDateTime.AddMinutes(5);) we get the following:

Executing HTTP request: {
  "requestId": "08354276-34b2-4183-8884-9fc92fbac13d",
  "method": "POST",
  "uri": "/api/HumanPatternExample_HttpStart"
}
Executing 'HumanPatternExample_HttpStart' 
Started orchestration with ID = 'e660e4ee29044d8ea9bbbcff0e7e001f'.
Executed 'HumanPatternExample_HttpStart' (Succeeded, Id=6b33326d-b0e5-4fd1-9671-43f176b12928)
Executing 'HumanPatternExample_Orchestrator' (Reason='', Id=e2845e74-9818-4a92-ab29-e85233c208f3)
************** RunOrchestrator method executing ********************
Function 'HumanPatternExample_RequestApproval (Activity)' started.
Executing 'HumanPatternExample_RequestApproval' (Reason='', Id=b25f71e4-810f-4fc4-bb71-0c8f819eccfc)
Requesting approval for comment: I LOOOOVEEEE cheese.
Sending SMS: 'I LOOOOVEEEE cheese'
Approve: http://localhost:7071/api/HumanPatternExample_Approve?id=e660e4ee29044d8ea9bbbcff0e7e001f
Decline: http://localhost:7071/api/HumanPatternExample_Decline?id=e660e4ee29044d8ea9bbbcff0e7e001f
Executed 'HumanPatternExample_RequestApproval' (Succeeded, Id=b25f71e4-810f-4fc4-bb71-0c8f819eccfc)
Function 'HumanPatternExample_Orchestrator (Orchestrator)' is waiting for input. Reason: CreateTimer
Function 'HumanPatternExample_Orchestrator (Orchestrator)' is waiting for input. Reason: WaitForExternalEvent:Moderation
Function 'HumanPatternExample_Orchestrator (Orchestrator)' was resumed by a timer scheduled for '2019-09-05T05:42:05.9852935Z'. State: TimerExpired
************** Comment 'I LOOOOVEEEE cheese' was not reviewed by a moderator in time, escalating...  ********************
************** Orchestration complete ********************
Executed 'HumanPatternExample_Orchestrator' (Succeeded, Id=eff52ac6-a7e8-4d81-afd5-9125bd5a5aaa)

Notice this time the orchestration was “un-paused” because the timer expired and the moderator didn’t respond: Function 'HumanPatternExample_Orchestrator (Orchestrator)' was resumed by a timer scheduled for '2019-09-05T05:42:05.9852935Z'. State: TimerExpired

The sample code in this article does not contain comprehensive error handling or security so you’d want to make sure you have both these in place if you were to implement this kind of workflow.

You can also use the Durable Functions API to send events though you’ll need to expose the system key which you probably won’t want to do – read Part 9: The Asynchronous HTTP API Pattern to learn more.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Understanding Azure Durable Functions - Part 10 The Monitor Pattern

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

In the previous part in this series, we looked at the  Asynchronous HTTP API Pattern where a client can poll to see if an orchestration has completed or not.

The monitor pattern is like a mirror image of this, whereby the orchestration polls some service at regular intervals. For example an orchestration could  initiate a long running asynchronous process and then periodically poll to see if the operation is complete. Once the operation is complete the orchestration can complete (or continue with more operations.)

We can create a pause in the execution of an orchestration by calling the CreateTimer method of the DurableOrchestrationContext. This method takes a DateTime specifying how long to “sleep” for, and a CancellationToken.

As an example, suppose we want to allow a client to post a video to be encoding to a different format.

The fist client function could look something like:

[FunctionName("MonitorPatternExample_HttpStart")]
public static async Task<HttpResponseMessage> HttpStartV1(
    [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{            
    dynamic data = await req.Content.ReadAsAsync<dynamic>();
    var fileName = data.FileName;

    string instanceId = await starter.StartNewAsync("MonitorPatternExample", fileName);

    return starter.CreateCheckStatusResponse(req, instanceId);
}

For simplicity, we just taking a filename to be used as a job identifier. This function calls the orchestrator function MonitorPatternExample.

The orchestrator function is where the Monitor Pattern is implemented:

[FunctionName("MonitorPatternExample")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    string fileName = context.GetInput<string>();

    // start encoding
    await context.CallActivityAsync<string>("MonitorPatternExample_BeginEncode", fileName);


    // We don't want the orchestration to run infinitely
    // If the operation has not completed within 30 mins, end the orchestration
    var operationTimeoutTime = context.CurrentUtcDateTime.AddMinutes(30);
  
    while (true)
    {
        var operationHasTimedOut = context.CurrentUtcDateTime > operationTimeoutTime;

        if (operationHasTimedOut)
        {
            context.SetCustomStatus("Encoding has timed out, please submit the job again.");
            break;
        }

        var isEncodingComplete = await context.CallActivityAsync<bool>("MonitorPatternExample_IsEncodingComplete", fileName);

        if (isEncodingComplete)
        {
            context.SetCustomStatus("Encoding has completed successfully.");
            break;
        }

        // If no timeout and encoding still being processed we want to put the orchestration to sleep,
        // and awaking it again after a specified interval
        var nextCheckTime = context.CurrentUtcDateTime.AddSeconds(15);
        log.LogInformation($"************** Sleeping orchestration until {nextCheckTime.ToLongTimeString()}");
        await context.CreateTimer(nextCheckTime, CancellationToken.None);
    }
}

This code starts the actual asynchronous/long-running work by calling the MonitorPatternExample_BeginEncode activity.

Then we loop around a while loop until either the long running operation is completes or a timeout occurs.

To query whether or not the encoding is complete, the MonitorPatternExample_IsEncodingComplete activity is called.

The timeout in this example is fixed at 30 mins: var operationTimeoutTime = context.CurrentUtcDateTime.AddMinutes(30);

The orchestration is put to sleep for 15 seconds before the while loop starts again with the code:

var nextCheckTime = context.CurrentUtcDateTime.AddSeconds(15);
log.LogInformation($"************** Sleeping orchestration until {nextCheckTime.ToLongTimeString()}");
await context.CreateTimer(nextCheckTime, CancellationToken.None);

Now the orchestration will either complete by timing out with a custom status context.SetCustomStatus("Encoding has timed out, please submit the job again."); or when the encoding has completed context.SetCustomStatus("Encoding has completed successfully."); 

Just a quick reminder that you can start watching my Pluralsight courses with a free trial.

 

The full listing is as follows:

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace DurableDemos
{
    public static class MonitorPatternExample
    {
        [FunctionName("MonitorPatternExample_HttpStart")]
        public static async Task<HttpResponseMessage> HttpStartV1(
            [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
            [OrchestrationClient]DurableOrchestrationClient starter,
            ILogger log)
        {            
            dynamic data = await req.Content.ReadAsAsync<dynamic>();
            var fileName = data.FileName;

            string instanceId = await starter.StartNewAsync("MonitorPatternExample", fileName);

            return starter.CreateCheckStatusResponse(req, instanceId);
        }

            [FunctionName("MonitorPatternExample")]
            public static async Task RunOrchestrator(
                [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
            {
                string fileName = context.GetInput<string>();

                // start encoding
                await context.CallActivityAsync<string>("MonitorPatternExample_BeginEncode", fileName);


                // We don't want the orchestration to run infinitely
                // If the operation has not completed within 30 mins, end the orchestration
                var operationTimeoutTime = context.CurrentUtcDateTime.AddMinutes(30);
          
                while (true)
                {
                    var operationHasTimedOut = context.CurrentUtcDateTime > operationTimeoutTime;

                    if (operationHasTimedOut)
                    {
                        context.SetCustomStatus("Encoding has timed out, please submit the job again.");
                        break;
                    }

                    var isEncodingComplete = await context.CallActivityAsync<bool>("MonitorPatternExample_IsEncodingComplete", fileName);

                    if (isEncodingComplete)
                    {
                        context.SetCustomStatus("Encoding has completed successfully.");
                        break;
                    }

                    // If no timeout and encoding still being processed we want to put the orchestration to sleep,
                    // and awaking it again after a specified interval
                    var nextCheckTime = context.CurrentUtcDateTime.AddSeconds(15);
                    log.LogInformation($"************** Sleeping orchestration until {nextCheckTime.ToLongTimeString()}");
                    await context.CreateTimer(nextCheckTime, CancellationToken.None);
                }
            }

        [FunctionName("MonitorPatternExample_BeginEncode")]
        public static void BeginEncodeVideo([ActivityTrigger] string fileName, ILogger log)
        {
            // Call API, start an async process, queue a message, etc.
            log.LogInformation($"************** Starting encoding of {fileName}");

            // This activity returns before the job is complete, its job is to just start the async/long running operation
        }


        [FunctionName("MonitorPatternExample_IsEncodingComplete")]
        public static bool IsEncodingComplete([ActivityTrigger] string fileName, ILogger log)
        {
            log.LogInformation($"************** Checking if {fileName} encoding is complete...");
            // Here you would make a call to an API, query a database, check blob storage etc 
            // to check whether the long running asyn process is complete

            // For demo purposes, we'll just signal completion every so often
            bool isComplete = new Random().Next() % 2 == 0;

            log.LogInformation($"************** {fileName} encoding complete: {isComplete}");

            return isComplete;
        }


    }
}

SHARE:

Understanding Azure Durable Functions - Part 9: The Asynchronous HTTP API Pattern

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

If your orchestration takes a while to execute, you may not want the end client (for example a web app that triggers the orchestration via an HTTP call) to wait around for a response. Instead you may want to provide the client with a way of querying (polling) if the long-running process is complete. In a previous article in this series we looked at how to get results from orchestrations. In this article we’ll dig into this in a bit more detail.

The Asynchronous HTTP API Pattern means the client calls an HTTP API which does not return the end result, but rather returns a way of checking for the completion of the task, for example by being providing with a status URL. This pattern may also be referred to as the Polling Consumer Pattern.

Recall from this previous article that when the client HTTP function is called, it returns some body content with management URLS for the orchestration instance that was started, for example:

{
    "id": "1bf95a9fa5084745bce24363e9ee781b",
    "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "rewindPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA=="
}

Notice that this information provides the client with a lot of information, including the URLs to terminate the orchestration, purge the history etc. Notice the response also include the key:  code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==

In the Azure Portal, this is the durabletask_extension key for the function app. With this key the client can perform admin/management operations on orchestration instances using the API including getting results from arbitrary orchestrations, terminating running orchestrations, etc..

The response also contains headers, including one called Location that also points to the check status URL, e.g. http://localhost:7071/runtime/webhooks/durabletask/instances/1bf95a9fa5084745bce24363e9ee781b?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==

If we look at the client function, this information is generated with the line: return starter.CreateCheckStatusResponse(req, instanceId);

[FunctionName("AsyncApiPatternExample_HttpStartV1")]
public static async Task<HttpResponseMessage> HttpStartV1(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    string instanceId = await starter.StartNewAsync("AsyncApiPatternExample", null);

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

    return starter.CreateCheckStatusResponse(req, instanceId);
}

You probably do not want to expose the durabletask_extension key to clients as this will allow them to perform operations they should not have access to. Instead we can modify the client function as follows:

[FunctionName("AsyncApiPatternExample_HttpStartV2")]
public static async Task<HttpResponseMessage> HttpStartV2(
    [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    string instanceId = await starter.StartNewAsync("AsyncApiPatternExample", null);

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

    // Create the URL to allow the client to check status of a request (excluding the function key in the code querystring)
    string checkStatusUrl = string.Format("{0}://{1}/api/AsyncApiPatternExample_Status?id={2}", req.RequestUri.Scheme, req.RequestUri.Host, instanceId);

    // Create the response and add headers
    var response = new HttpResponseMessage()
    {
        StatusCode = System.Net.HttpStatusCode.Accepted,                
        Content = new StringContent(checkStatusUrl),                
    };
    response.Headers.Add("Location", checkStatusUrl);
    response.Headers.Add("Retry-After", "10");

    return response;
}

In this new version of the client function, we control what is passed back to the client, we don’t include any sensitive management URLs/keys, a response from calling this function would look like:

Response body (text): https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55
Response status: 202Accepted
Response Headers:
Location = https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55
Retry-After = 10
etc.

The client can then GET the status URL (+ the function key): https://localhost:7071/api/AsyncApiPatternExample_Status?id=d69a847230e5411ca57659723cb14c55&code=XXXXXXXXXX

This will return:

{
    "currentStatus": "Running",
    "result": null
}

And once the orchestration has complete this will return:

{
    "currentStatus": "Completed",
    "result": "Hello London!"
}

The actual status function look like the following:

 

[FunctionName("AsyncApiPatternExample_Status")]
public static async Task<IActionResult> Status(
  [HttpTrigger(AuthorizationLevel.Function, "get")]HttpRequest req,
  [OrchestrationClient]DurableOrchestrationClient orchestrationClient,
  ILogger log)
{
    var orchestrationInstanceId = req.Query["id"];

    if (string.IsNullOrWhiteSpace(orchestrationInstanceId))
    {
        return new NotFoundResult();
    }

    // Get the status for the passed in instanceId
    DurableOrchestrationStatus status = await orchestrationClient.GetStatusAsync(orchestrationInstanceId);

    if (status is null)
    {
        return new NotFoundResult();
    }

    
    var shortStatus = new
    {
        currentStatus = status.RuntimeStatus.ToString(),
        result = status.Output
    };

    return new OkObjectResult(shortStatus);
    //  We could also expand this and check status.RuntimeStatus and for example return a 202 if processing is still underway
}

The key thing in the preceding code is the call: DurableOrchestrationStatus status = await orchestrationClient.GetStatusAsync(orchestrationInstanceId); This allows the status to be obtained for the orchestration id that was passed in as a querystring parameter.

The output to the client is chosen in the anonymous object shortStatus. Now the client does not get sensitive information returned such as the management URLs and keys. A client could however still retrieve the status/result from orchestrations started by other clients.If you want more fine grained control/authentication you should check out the other options in the documentation.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE: