Skip to content
This repository has been archived by the owner on Sep 24, 2020. It is now read-only.

Metasys10.1 update #4

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Tutorial for C#
# Tutorial for C\#

All of the lessons in this repository take advantage of the latest version of .Net

You can run the examples directly from the command line using `dotnet` command, or
in Visual Studio.

## Installing Prequisites
## Installing Prerequisites

You will need

* .NET Core 2.1
* A Text Editor (optional)
* Git (optional)

You also need credentials for and access to a Metasys Server.
You also need credentials for and access to a Metasys Server.

### .NET Core

Expand Down Expand Up @@ -43,7 +43,7 @@ Git isn't required to follow along so you can skip it if you wish.

If you wish you can just browse the tutorials in your browser. Simply click on the folders
for each lesson. Each of them includes a Program.cs file with code and a README that explains
the code.
the code.

However, it is recommended that you download the repository. When you do that you can
run each program, debug each one, and even modify it to explore the API.
Expand Down Expand Up @@ -71,7 +71,7 @@ Then you can unzip the archive and browse the code and tutorials.
## Running The Programs

Each program can be run exactly the same way using the `dotnet run` command.
Each program requires that you supply a usernmae, password and hostname for your
Each program requires that you supply a username, password and hostname for your
Metasys server.

To run them open a console. Change directory to a lesson and type the following
Expand All @@ -82,11 +82,13 @@ dotnet run username password hostname

## Troubleshooting

* Self signed certificatess. If you client computer doesn't trust the certificate on your Metasys server the program will fail to run. You'll see an exception similar to the following:
* Self signed certificates. If you client computer doesn't trust the certificate on your Metasys server the program will fail to run. You'll see an exception similar to the following:

Unhandled Exception: System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure'
```shell
Unhandled Exception: System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure'
```

To fix this issue you must obtain a copy of the certificate from the server and import into your trusted certifidate store.
* There are various ways of getting a copy of the certificate. Follow the steps at this stackoverflow question: <https://superuser.com/questions/97201/how-to-save-a-remote-server-ssl-certificate-locally-as-a-filej/> (*Note: It is you responsibility to verify the certificate before marking it as trusted. Consult your Metasys administrator if you have questions about the validity of your certificate.*)
* Mark them as trusted in your OS. On a windows client computer you can following the instructions in *Appendix: Certificate Management* of <http://cgproducts.johnsoncontrols.com/MET_PDF/12011279.pdf/>. See the section entitled *Importing Root and Intermediate Certificates*
To fix this issue you must obtain a copy of the certificate from the server and import into your trusted certificate store.

* There are various ways of getting a copy of the certificate. Follow the steps at this stackoverflow question: <https://superuser.com/questions/97201/how-to-save-a-remote-server-ssl-certificate-locally-as-a-filej/> (*Note: It is you responsibility to verify the certificate before marking it as trusted. Consult your Metasys administrator if you have questions about the validity of your certificate.*)
* Mark them as trusted in your OS. On a windows client computer you can following the instructions in *Appendix: Certificate Management* of <http://cgproducts.johnsoncontrols.com/MET_PDF/12011279.pdf/>. See the section entitled *Importing Root and Intermediate Certificates*
21 changes: 7 additions & 14 deletions lesson1/Program.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
using System;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace tutorial_csharp
namespace lesson1
{
public class Program
{

public static async Task Main(string[] args)
{
var username = args[0];
var password = args[1];
var hostname = args[2];


using (var client = new HttpClient {BaseAddress = new Uri($"https://{hostname}/api/v1")})
using (var client = new HttpClient {BaseAddress = new Uri($"https://{hostname}/api/v2")})
{
// Login - constructing a payload that looks like { "username": "thename', "password": "thepassword" }
var loginMessage = $"{{ 'username': '{username}', 'password': '{password}' }}";
Expand All @@ -29,7 +27,6 @@ public static async Task Main(string[] args)

var loginResponseMessage = await client.PostAsync("login", loginContent);


// Read the results
var loginResult = await loginResponseMessage.Content.ReadAsStringAsync();

Expand All @@ -44,15 +41,11 @@ public static async Task Main(string[] args)
// Let's attempt to retrieve the first page of alarms
var alarmsResponse = await client.GetAsync("alarms");

// Oops we get a 401
// End of lesson
Console.WriteLine($"Status code returned by alarms endpoint: {alarmsResponse.StatusCode}");

// Parse the response using Newtonsoft and print the first one.
var alarmsObject = JObject.Parse(await alarmsResponse.Content.ReadAsStringAsync());
var alarms = alarmsObject["items"];
Console.WriteLine($"First alarm: {alarms?[0]}");
}



}

}
}
54 changes: 38 additions & 16 deletions lesson1/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Lesson 1

In this lesson we show you how to login and get an access token.
In this lesson we show you how to login and get an access token. Then we'll use
that token to retrieve a page of alarms and print the first one.

Dependencies:

Expand All @@ -19,7 +20,26 @@ You should see something like the following:
$ dotnet run myname mypassword myhostname
The login request payload: { 'username': 'myname', 'password': 'mypassword' }
Your accessToken was successfully retrieved. Remember to always protect your access tokens.
Status code returned by alarms endpoint: Unauthorized
First alarm: {
"self": "https://adx01/api/alarms/ce480de8-9e75-4525-a6a7-b163ec898ced",
"id": "ce480de8-9e75-4525-a6a7-b163ec898ced",
"itemReference": "ADX01:NAE1/FC-B.VMA-12.ZN-T",
"name": "ZN-T",
"message": "",
"isAckRequired": false,
"typeUrl": "https://adx01/api/enumSets/108/members/0",
"priority": 200,
"triggerValue": {
"value": "71.2",
"unitsUrl": "https://adx01/api/enumSets/507/members/64"
},
"creationTime": "2019-03-26T04:07:33Z",
"isAcknowledged": false,
"isDiscarded": false,
"categoryUrl": "https://adx01/api/enumSets/33/members/5",
"objectUrl": "https://adx01/api/objects/1c031654-9fc2-5648-883c-d202cb3bdc7d",
"annotationsUrl": "https://adx01/api/alarms/ce480de8-9e75-4525-a6a7-b163ec898ced/annotations"
}
```

The code for this program is all in one file [Program.cs](./Program.cs)
Expand All @@ -39,7 +59,7 @@ var hostname = args[2];
Next we create an HttpClient

```csharp
using (var client = new HttpClient {BaseAddress = new Uri($"https://{hostname}/api/v1")})
using (var client = new HttpClient {BaseAddress = new Uri($"https://{hostname}/api/v2")})
{
...
}
Expand All @@ -53,7 +73,7 @@ the hostname.
In this section we'll see how to construct a login request, send it to the server
and then retrieve the access token from the server's response.

If you look at the [documentation](https://metasys-server.github.io/api-docs/#/reference/authentication/login/login) for
If you look at the [documentation](https://metasys-server.github.io/api-landing/api/v2/#/reference/authentication/login/login) for
the login API you see that we need to construct a JSON payload that looks like the following:

```json
Expand Down Expand Up @@ -111,8 +131,7 @@ The login result should be a string that look likes the following
}
```

(The `accessToken` was truncated in the above example for formatting reasons; and also because you don't want to
leak access tokens.)
(The `accessToken` was truncated in the above example for formatting reasons; and also because you don't want to leak access tokens.)

We want to get that access token. To do that we could use string methods, but it's much easier to use a
JSON library like Newtonsoft. Therefore, we use `JToken.Parse`. The following line parses the JSON,
Expand All @@ -125,12 +144,12 @@ var accessToken = JToken.Parse(loginResult)["accessToken"].Value<string>();
## Set the Authorization Header for future requests

Every other request we make needs the access token in an Authorization header. This is documented
on every call. See [Get Alarms](https://metasys-server.github.io/api-landing/api/alderaan/#/reference/alarms/get-alarms/get-alarms) for an example.
on every call. See [Get Alarms](https://metasys-server.github.io/api-landing/api/v2/#/reference/alarms/get-alarms/get-alarms) for an example.
If you look in the right hand pane and scroll down a bit you'll see the Headers:

<img alt="Request Headers" src="./images/headers.png" width=400px>

The Accept header is optional as explained in the [versioning section](https://metasys-server.github.io/api-landing/api/alderaan/#/introduction/api-version). We are choosing to use the URL
The Accept header is optional as explained in the [versioning section](https://metasys-server.github.io/api-landing/api/v1/#/introduction/api-version/api-version-notes). We are choosing to use the URL
to specify version.

The Authorization header is not optional. It must be on every request.
Expand All @@ -141,9 +160,9 @@ So we set this header as follows:
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse($"Bearer {accessToken}");```
```

## Attempt to Make Another Call
## Make Another Call

Finally we attempt to make another call:
Finally we use the token we just retrieved to make another call:

```csharp
var alarmsResponse = await client.GetAsync("alarms");
Expand All @@ -152,10 +171,13 @@ var alarmsResponse = await client.GetAsync("alarms");
Notice it's much easier to make a GET call then a POST. We didn't need to format any request. We just need
the URL fragment of the endpoint we want to call. In this case `alarms`.

Unfortunately this call will return a `401` status code, `Unauthorized`.

Why? The reason is a little complicated. First you should know that the server's response to any call is to return a redirect. These are instructions from the server to the client stating that the URL they attempted to access has been moved to a different location. By default, HttpClient will follow these redirects automatically for you.
But for security reasons, it strips off the Authorization header we just constructed. So the call fails and tells you
that you are unauthorized.
Assuming there were no problems with the previous call we can use `JObject` to parse the
string content of the response. Then we can access the `"items"` property on the object
which contains a list of alarms. Finally we access the first one (if the list is not empty)
and print it.

Go on to Lesson 2 where we'll demonstrate our first method for resolving this issue.
```csharp
var alarmsObject = JObject.Parse(await alarmsResponse.Content.ReadAsStringAsync());
var alarms = alarmsObject["items"];
Console.WriteLine($"First alarm: {alarms?[0]}");
```
Binary file modified lesson1/images/headers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion lesson1/tutorial-csharp.csproj → lesson1/lesson1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RootNamespace>tutorial_csharp</RootNamespace>
<LangVersion>7.1</LangVersion>
</PropertyGroup>

Expand Down
72 changes: 38 additions & 34 deletions lesson2/Program.cs
Original file line number Diff line number Diff line change
@@ -1,65 +1,69 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System;
using System.Threading.Tasks;
using Flurl.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace lesson2
{
public class Program
{

public static async Task Main(string[] args)
{
var username = args[0];
var password = args[1];
var hostname = args[2];

var handler = new HttpClientHandler {AllowAutoRedirect = false};
string accessToken;

using (var client = new HttpClient(handler) {BaseAddress = new Uri($"https://{hostname}/api/v1")})
using (var client = new FlurlClient($"https://{hostname}/api/v2"))
{
// Login - constructing a payload that looks like { "username": "thename', "password": "thepassword" }
var loginMessage = $"{{ 'username': '{username}', 'password': '{password}' }}";
var loginResult = await client.Request("login")
.PostJsonAsync(new {username, password})
.ReceiveJson();

var loginContent = new StringContent(loginMessage,
Encoding.UTF8,
"application/json");
// loginResult is a dynamic object. It has dynamic properties based on the JSON received
accessToken = loginResult.accessToken;
}

var loginResponseMessage = await client.PostAsync("login", loginContent);
// Create a new client that has the OAuthHeader set on each request.
using (var client = new FlurlClient($"https://{hostname}/api/v2").WithOAuthBearerToken(accessToken))
{
var alarms = await client.Request("alarms").GetJsonAsync();

// Handle the redirect
loginResponseMessage = await client.PostAsync(loginResponseMessage.Headers.Location, loginContent);
// Again alarms is a dynamic object, we can query for any property
// defined in the schema

var nextPageUrl = alarms.next;
Console.WriteLine($"The next page of alarms url: {nextPageUrl}");

// Read the results
var loginResult = await loginResponseMessage.Content.ReadAsStringAsync();
var firstAlarmItemReference = alarms.items[0].itemReference;
Console.WriteLine($"The item reference of the first alarm: {firstAlarmItemReference}");

// Get the access token
// We could use string manipulation methods, but using JSON.NET is much easier
var accessToken = JToken.Parse(loginResult)["accessToken"].Value<string>();
var triggerValueUnits = alarms.items[0].triggerValue.unitsUrl;
Console.WriteLine($"The trigger value unitsUrl of first alarm: {triggerValueUnits}");

// But the bad thing is if we just want to get an Alarm
// it doesn't work like we want
var firstAlarm = alarms.items[0];
Console.WriteLine($"The first alarm (oops!): {firstAlarm.ToString()}"); // Outputs "System.Dynamic.ExpandoObject"

// Add an authorization header to the list of default headers for our client
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse($"Bearer {accessToken}");
Console.WriteLine($"The first alarm (much better): {JsonConvert.SerializeObject(firstAlarm, Formatting.Indented)}");

// So an alternative is to ues GetJsonAsync<JToken> which returns JTokens as defined
// by Newtonsoft. To illustrate let's fetch the alarms again

// Let's attempt to retrieve the first page of alarms
var alarmsResponse = await client.GetAsync("alarms");
var alarmsObject = await client.Request("alarms").GetJsonAsync<JToken>();

// Now we can treat alarms as a dictionary
var alarmsCollection = alarmsObject["items"]; // Returns a JArray since items is a collection
var firstAlarmObject = alarmsCollection[0]; // Returns a JObject since each alarm is an object

// Handle the redirect
alarmsResponse = await client.GetAsync(alarmsResponse.Headers.Location);
Console.WriteLine($"The first alarm (using JToken): {firstAlarmObject}");

// Parse the response using Newtonsoft and print the first one.
var alarmsObject = JObject.Parse(await alarmsResponse.Content.ReadAsStringAsync());
var alarms = alarmsObject["items"];
Console.WriteLine($"First alarm: {alarms?[0]}");
var firstAlarmObjectItemReference = alarmsCollection[0]["itemReference"];
Console.WriteLine($"The item reference of the first alarm: {firstAlarmObjectItemReference}");
}



}

}
}
Loading