New Azure Functions SDK and VSTS - how to build your functions?

With recent release of Visual Studio 2017 15.3 we're given the possibility to use Azure Functions SDK, which is now fully integrated with IDE. This improves development greatly as we no longer have to maintain function.json by ourselves and are able to run function locally using in-built runtime. How about integrating new features into existing CI/CD pipeline? Well, there're some gotchas, fortunately you can easily configure things so everything runs smoothly.

Building a function project

When you create a new function project in VS you're given the options to easily add new functions with a boilerplate code. If you investigate .csproj file(last time I wrote about building Azure Functions using VSTS it was a .funproj file), you'll realize, that it differs greatly compared to legacy .csproj files:

/
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net461</TargetFramework>
  </PropertyGroup>
  <ItemGroup>    
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.0" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="Microsoft.CSharp" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Considering new structure you can imagine, that it's going to give us some troubles. If you try to build it in VSTS, you'll get following error:

/
Error MSB4066: The attribute "Version" in element <PackageReference> is unrecognized.

Apparently using a default agent queue won't work here - agents don't have proper targets, which are required to build a project(if you build a project locally, you can find those targets in C:\Users\{USER}\.nuget\packages\microsoft.net.sdk.functions\1.0.0\build\netstandard1.0\Microsoft.NET.Sdk.Functions.Build.targets). It'd possible to just import them and run on a default agent, still this looks like a workaround. However, there's an easier solution - during scheduling of your build you can choose a different queue:

Let's try to build our project once more...

/
 Error MSB4041: The default XML namespace of the project must be the MSBuild XML namespace. If the project is authored in the MSBuild 2003 format, please add xmlns="http://schemas.microsoft.com/developer/msbuild/2003" to the <Project> element. If the project has been authored in the old 1.0 or 1.2 format, please convert it to MSBuild 2003 format.

There's still a problem - MSBuild is not able to determine what kind of XML is this. To fix this you can just modify <Project> node to following:

/
<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

Now your project should build correctly:

When working on this solution I found, that some people tried to build new Azure Functions projects using dotnet build step in VSTS - unfortunately this didn't work for me. Let me know if you found another way of integrating new version of .csproj with VSTS!

You shall not forget - reminding about pull requests in VSTS #2

In the previous post I presented you how easily we can determine whether a PR has been reviewed or not. Now we'll create a real solution, which will send a notification to a Slack channel containing information about waiting pull request.

Creating an Azure Function

To handle our solution we'll develop a function, which will do following:

  • call VSTS API to check whether there're active PRs
  • filter those PRs, which haven't been reviewed during a specific interval
  • send a message to a Slack channel with specific information regarding forgotten PRs

Now let's create a function. Go to Azure Portal and create a new Function App(if you don't have one). Once created add a function as a TimerTrigger(schedule doesn't matter in this moment). Once you're done, you should get following function:

Now - to authenticate our calls to the VSTS API we'll need a personal access token. To create one go to the instance of VSTS you're going to use and go to Security screen.

On the left you should see Personal access tokens tab. Go there and create a new personal access token, which we'll use in our function.

Once we have a PAT we can write add some code to our function - for this moment we'll try to check whether there're active PRs.

Fetching active PRs from VSTS

This is a simple task - we have to call an API with a generated PAT. Consider following code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "THIS_IS_YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            using (HttpResponseMessage response = client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0").Result)
            {
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();
                log.Info(responseBody);
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

When you replace dummy values with values of your own, you should receive in the Logs window a serialized response containing active pull requests(of course if there's any). So far so good - let's try to find if any requires our attention.

Finding outdated PRs

To find outdate PRs we'll have to change our code a little - for now we have a raw string and are unable to query any of its properties. For the purpose of this post I decided to use a dynamic object, just not to trouble with creating a DTO for a response. To do so just create responseBody variable to:

/
dynamic data = await response.Content.ReadAsAsync<object>();

No we can easily query PRs and find those, which haven't been reviewed during a specific interval(let's say 24 hours). Let's add following code to our function:

/
using (HttpResponseMessage response = client.GetAsync(
			"https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{project}/pullRequests?api-version=3.0").Result)
{
	response.EnsureSuccessStatusCode();
	dynamic data = await response.Content.ReadAsAsync<object>();

	foreach(var pr in data.value) {
		foreach(var reviewer in pr.reviewers) {
			if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
				log.Info($"Reviewer {reviewer.displayName} still hasn't reviewed a PR!");
			}
		}
	}
}

With the above code we're able to find reviewers, which haven't got a chance to take a look and review a pull request. In the last part we'll send this information to a Slack channel.

Notifying in a Slack channel

To send something to a Slack channel we have to set up a webhook integration which is described here. Once you have added a webhook to your channel and got an endpoint, which can be used to send messages to a channel, we can extend our function. Here you have the complete code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            dynamic data = null;
            using (HttpResponseMessage response = await client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0"))
            {
                response.EnsureSuccessStatusCode();
                data = await response.Content.ReadAsAsync<object>();
            }

            foreach(var pr in data.value) {
                foreach(var reviewer in pr.reviewers) {
                    if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
                        var textToSend = $"Reviewer {reviewer.displayName} still hasn't reviewed a PR!";

                        await client.PostAsync("https://hooks.slack.com/{...}", new StringContent("{\"text\":\"" + textToSend + "\"}"));
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

Summary

VSTS and its API give you many flexibility and are really fun to play with. The solution presented here is not ideal - there's still a possibility to improve things. Anyway, I'd like to encourage you to your own experiments with VSTS and different integrations - you'll be surprised what you can achieve with a few lines of code.