dng

Working with PI Web API - HttpClient in C#

Blog Post created by dng Employee on Aug 11, 2015

As I was trying to write a C# application that uses PI Web API and looking up various ways to make a REST call in .NET applications, I came across the following methods to consume REST APIs in .NET:

  • WebClient
  • HttpWebRequest
  • HttpClient
  • Other libraries (e.g. RestSharp)

 

It looks like the new API that Microsoft offers, HttpClient (namespace: System.Net.Http), provides some powerful functionality with better syntax support for newer threading features. For more information about the differences of various ways to make REST calls, please refer to the following resources:

 

Looking through the pros and cons of each, I have decided to give HttpClient a try. In the following blog post, we will be writing a wrapper around HttpClient for easy access to PI Web API. Note that this is not a comprehensive HttpClient wrapper for PI Web API, the idea is to share some development thoughts while testing the class out. If you need a quick and easy way to access PI Web API in your development project, you can use this class and focus on the data access logic.

 

The following example is created in Visual Studio 2013, .NET 4.5 and tested with PI Web API 2015 R2.

 

 

Getting Started

First, we will create a wrapper. Start a new Visual Studio project and name the class PIWebAPIClient.

 

namespace piwebapi_cs_helper
{
    public class PIWebAPIClient
    {
    }
}

 

Since we will be handling JSON objects, let’s add the Newtonsoft.Json package from the package manager console:

Install-Package Newtonsoft.Json

 

And add the using directive:

using Newtonsoft.Json.Linq;

 

Let’s also add a reference to System.Net.Http and add the using directive since we are testing the HttpClient class!

using System.Net.Http

 

 

Creating and Disposing the HttpClient

In the following, we will write constructors and methods to initialize a HttpClient intended to be used to make multiple HTTP requests.

 

        private HttpClient client;

        /* Initiating HttpClient using the default credentials.
         * This can be used with Kerberos authentication for PI Web API. */
        public PIWebAPIClient()
        {
            client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
        }

        /* Initializing HttpClient by providing a username and password. The basic authentication header is added to the HttpClient.
         * This can be used with Basic authentication for PI Web API. */
        public PIWebAPIClient(string userName, string password)
        {
            client = new HttpClient();
            string authInfo = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(String.Format("{0}:{1}", userName, password)));
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authInfo);
        }

        /* Disposing the HttpClient. */
        public void Dispose()
        {
            client.Dispose();
        }

 

As a simple example, we did not add any additional headers for the HttpClient. Feel free to add headers appropriate for your application. In addition, although HttpClient does implement the IDisposable interface, many MSDN examples did not explicitly call Dispose(). We will include it for completion sake.

 

 

Asynchronous GET and POST request

We are ready to make HTTP requests! Since PI Web API only returns JSON objects by default, we will return a NewtonSoft JObject from our async GET method. For POST, we will be accepting a string payload in JSON format. If the response message indicates that the request is not successful, a HttpRequestException will be thrown with the response message.

 

        /* Async GET request. This method makes a HTTP GET request to the uri provided
         * and throws an exception if the response does not indicate a success. */
        public async Task<JObject> GetAsync(string uri)
        {
            HttpResponseMessage response = await client.GetAsync(uri);
            string content = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                var responseMessage = "Response status code does not indicate success: " + (int)response.StatusCode + " (" + response.StatusCode + " ). ";
                throw new HttpRequestException(responseMessage + Environment.NewLine + content);
            }
            return JObject.Parse(content);
        }

        /* Async POST request. This method makes a HTTP POST request to the uri provided
         * and throws an exception if the response does not indicate a success. */
        public async Task PostAsync(string uri, string data)
        {
            HttpResponseMessage response = await client.PostAsync(uri, new StringContent(data, Encoding.UTF8, "application/json"));
            string content = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                var responseMessage = "Response status code does not indicate success: " + (int)response.StatusCode + " (" + response.StatusCode + " ). ";
                throw new HttpRequestException(responseMessage + Environment.NewLine + content);
            }
        }

 

At times, you might need to submit other HTTP requests to PI Web API (e.g. PUT, PATCH). While you can use HttpClient.PutAsync for a PUT request, it doesn’t have a method to support PATCH request out-of-the-box. If you are in a situation to make a PATCH call, there are many online examples to do so. For more information, refer to this previous discussion.

 

 

(Optional) Additional methods to make Synchronous Calls (e.g. Console Application)

If you are writing a simple console application and would like to synchronously call the GetAsync/PostAsync method, you can add the following methods:

 

        /* Calling the GetAsync method and waiting for the results. */
        public JObject GetRequest(string url)
        {
            Task<JObject> t = this.GetAsync(url);
            t.Wait();
            return t.Result;
        }

        /* Calling the PostAsync method and waiting for the results. */
        public void PostRequest(string url, string data)
        {
            Task t = this.PostAsync(url, data);
            t.Wait();
        }

 

 

Testing synchronous calls in a Console application

Let’s build the solution and test!

 

First, let’s add references to our PIWebAPIClient helper, as well as to Newtonsoft.Json.

using piwebapi_cs_helper;
using Newtonsoft.Json.Linq;

 

and write a simple console application. The following console application accepts a URL (i.e. the PI Web API REST endpoint) and prints out the response message. Note that we are using the PIWebAPIClient constructor that uses the default credentials. This method works when your PI Web API instance is set up using Kerberos.

namespace piwebapi_cs_console_test
{
    class Program
    {
        /* Console application that makes GET request to a specified URL and display the response
         * as a string to the console. */
        static void Main(string[] args)
        {
            PIWebAPIClient piWebAPIClient = new PIWebAPIClient();
            do
            {
                try
                {
                    Console.Write("Enter URL: ");
                    string url = Console.ReadLine();
                    JObject jobj = piWebAPIClient.GetRequest(url);
                    Console.WriteLine(jobj.ToString());
                }
                catch (AggregateException ex)
                {
                    foreach (var e in ex.InnerExceptions)
                    {
                        Console.WriteLine(e.Message);
                    }
                }
                finally
                {
                    Console.WriteLine("Press any key to continue (esc to exit)...");
                }

            } while (Console.ReadKey().Key != ConsoleKey.Escape);
            piWebAPIClient.Dispose();
            Console.ReadKey();
        }
    }
}

 

 

As you can see, we can make simple synchronous requests!

 

 

Testing asynchronous calls in a WPF application

Next, we will write a WPF application to test making asynchronous GET and POST requests. Again, add references to the following:

using piwebapi_cs_helper;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net.Http;

 

If you are interested, the WPF xaml configuration is as follow:

<Window x:Class="piwebapi_cs_wpf_test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="325" Width="500">
    <Grid Margin="0,0,0,11">
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="300" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="User name" />
        <Label Grid.Row="1" Grid.Column="0" Content="Password" />
        <Label Grid.Row="2" Grid.Column="0" Content="PI Point path" />
        <Label Grid.Row="3" Grid.Column="0" Content="Value to write" />
        <TextBox Grid.Row="0" Grid.Column="1" Margin="3"  Name="userNameTextBox" />
        <PasswordBox Grid.Row="1" Grid.Column="1" Margin="3" Name="pwBox" />
        <TextBox Grid.Row="2" Grid.Column="1" Margin="3"  Name="tagTextBox" />
        <TextBox Grid.Row="3" Grid.Column="1" Margin="3"  Name="valueTextBox" />
        <Button Grid.Row="4" Grid.Column="1" Margin="5" Content="Write" Name="writeBtn" Click="writeBtn_Click"/>
        <TextBlock Grid.Row="5" Grid.Column="1" Margin="3" Name="statusTextBlock" TextWrapping="WrapWithOverflow" />
    </Grid>
</Window>

 

The window accepts username and password combination and use basic authentication to connect to PI Web API. Any error (or success) messages are displayed in the bottom of window. It will first try to use the tag path specified to get to the endpoint that accepts a value-writing POST request. In this simple example, we will only be supplying the value in the payload. This means that the value will be written in current time.

namespace piwebapi_cs_wpf_test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /* This method takes the username and password specified to use basic authentication to connect to 
         * PI Web API. It then attempts to resolve the tag path provided and write to the tag. */
        private async void writeBtn_Click(object sender, RoutedEventArgs e)
        {
            string baseUrl = "https://dng-code.osisoft.int/piwebapi";
            string userName = userNameTextBox.Text;
            string password = pwBox.Password;
            string tagPath = tagTextBox.Text;
            PIWebAPIClient piWebAPIClient = new PIWebAPIClient(userName, password);

            try
            {
                //Resolve tag path
                string requestUrl = baseUrl + "/points/?path=" + tagPath;
                Task<JObject> tget = piWebAPIClient.GetAsync(requestUrl);
                statusTextBlock.Text = "Processing...";

                //Attempt to write value to the tag
                Object payload = new
                {
                    value = valueTextBox.Text
                };
                string data = JsonConvert.SerializeObject(payload);
                JObject jobj = await tget;
                await piWebAPIClient.PostAsync(jobj["Links"]["Value"].ToString(), data);

                //Display final results if successful
                statusTextBlock.Text = "Write success!";
            }
            catch (HttpRequestException ex)
            {
                statusTextBlock.Text = ex.Message;
            }
            catch (Exception ex)
            {
                statusTextBlock.Text = ex.Message;
            }
            finally
            {
                //We are closing the HttpClient after every write in this simple example. This is not necessary.
                piWebAPIClient.Dispose();
            }
        }
    }
}

 

Let's try to write to a tag!

 

Conclusion

The full VS 2013 solution (including the test project) is in the GitHub repository piwebapi-cs-helper. HttpClient looks pretty easy to use so far, and it works well with PI Web API. I am curious to see what others in the community have been using to access a PI Web API (or other REST API) in C#. Comments and feedback are welcome!

Outcomes