Marcos Vainer Loeff

ASP.NET Core 2 with PI AF SDK: Part 3 - Using Dependency Injection To Simplify Code And Extend Your Application

Blog Post created by Marcos Vainer Loeff Employee on Jul 8, 2019

Introduction

 

On the first two parts of this blog post series (part 1 and part 2), you have learned how to create an ASP.NET Core web application using PI AF SDK and .NET Framework. On the second part, you've learned how to secure your web app using ASP.NET Core Identity.

 

On this blog post, we are going to learn one of the best ASP.NET Core features which is Dependency Injection and apply this technique when making calls against our PI System.

 

 

What is Dependency Injection (DI)?

 

According to Wikipedia:

 

"In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service)."

 

Let's try to understand the concept of Dependency Injection through our example from the first part of this blog post series

 

When class A uses some functionality of class B, then it is said that class A has a dependency of class B. In the context of C#, class A needs to create an instance of class B in order to use its methods and properties. 

 

So, transferring the task of creating the object to another entity and directly using the dependency is called dependency injection.

 

In our sample, our HomeController class uses some PI AF SDK methods to render the snapshot values of the found PI Points to the user. We will make a small change in this project by creating a new service of the type IPISystemWrapper, responsible for all PI System communication which will be injected to our HomeController. As a result, the HomeController won't have any PI AF SDK code anymore. It will only inject the IPISystemWrapper service/interface and call its methods to deliver to the Views.

 

 

Why should I use DI when developing apps on top of the PI System?

 

Here are the main advantages of using DI in this context:

 

  • Easier to extend the application. Imagine that many controllers are injecting the IPISystemWrapper service. If you add a new method to this interface, then all the controllers will be able to take advantage of this new feature. As a result, this strategy helps reducing duplicated code.
  • Easier to maintain your code base. The service IPISystemWrapper could have different implementations. The AfSdkWrapper class could be the implementation for PI AF SDK of this service and the PIWebApiWrapper could be the implementation for using PI Web API. This is strategy improves your code base not only from the organization perspective but from the maintainability standpoint.
  • Enable loose coupling. Imagine that you must upgrade your web application from ASP.NET Core 2.2 to ASP.NET Core 3 once it is released. ASP.NET Core 3 cannot be created on top of .NET Framework and therefore, it won't be compatible with the current version of PI AF SDK. I will show you how easy it is to switch from PI AF SDK to another PI Developer Technology.
  • Helps Unit Testing: since all the PI AF SDK code will be written on a unique class. All your unit tests concerning PI AF SDK can be written for this particular class. If you have many controllers using PI AF SDK, it gets more complicated to achieve this goal.

 

Implementing DI on our example

 

This example is simpler than the one from part 1. This is why I am pasting the code from the main files of the VS project here. Nevertheless, please refer to the previous blog post in case something is not clear since I will focus on the DI concepts on this blog post.

 

Disclaimer:

Any of the code in this blog could contain bugs and shouldn’t be used in production without extensive testing.

You agree that if you use any of the provided code in your own production code that you accept all ownership, risks, liabilities, and responsibilities associated with the performance, support, and maintenance of the code.

 

Create a new ASP.NET Core project on top of .NET Framework and add PI AF SDK as a reference.

 

 

Create some Views with the content below in order to display the results to the user:

 

\Views\Home\Query.cshtml

 

<div class="col-sm-8 col-sm-offset-2">
<h1>View Snapshot Values</h1>
<br /><br />
<h4>Search for PI Points and check its snapshots.</h4>
<br /><br />
<div class="container">
<div class="row">
<form asp-controller="Home" asp-action="Result" method="get" class="form-inline">
<div class="form-group">
<label class="sr-only" for="name">Search PI Point: </label>
<input id="pointNameFilter" name="pointNameFilter" placeholder="Point Name" class="form-control" />
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>

 

\Views\Home\Result.cshtml

@model IEnumerable<PIPointSnapshotModelView>

<div class="col-sm-8 col-sm-offset-2">
<h1>View Snapshot Values</h1>
<br /><br />
<table class="table">
<tr>
<th>PI Point</th>
<th>Value</th>
<th>Timestamp</th>
</tr>
@foreach (var pointSnapshotValue in Model)
{
<tr>
<td>@pointSnapshotValue.PointName</td>
<td>@pointSnapshotValue.Value.ToString()</td>
<td>@pointSnapshotValue.TimeStamp.ToString("dd-MMM-yyyy hh:mm:ss tt")</td>
</tr>
}
</table>
<br />
<center>
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">Make another search</a>
</center>
</div>

 

\Views\Shared\_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>My ASP.NET Core MVC with PI AF SDK or PI Web API</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a asp-page="/Index" class="navbar-brand">Applying ASP.NET Core DI with the PI System </a>
</div>
<div class="navbar-collapse collapse">
</div>
</div>
</nav>
<div class="container body-content"style="padding-top: 50px;">
@RenderBody()
</div>
</body>
</html>

 

\Views\_ViewImports.cshtml

@using ASPNETCoreWithPIAFSDK.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

 

\Views\_ViewStart.cshtml

@{
Layout = "_Layout";
}

 

Create a model for the Result.cshtml view:

 

\Models\PIPointSnapshotModelView.cs

using OSIsoft.AF.Asset;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ASPNETCoreWithPIAFSDK.Models
{
public class PIPointSnapshotModelView
{
public string PointName { get; set; }

public object Value { get; set; }
public DateTime TimeStamp { get; set; }


public PIPointSnapshotModelView(string pointName, AFValue afValue)
{
PointName = pointName;
Value = afValue.Value;
TimeStamp = afValue.Timestamp;
}

public PIPointSnapshotModelView(string pointName)
{
PointName = pointName;
}
}
}

 

 

Let's create a folder named Framework on the root with the IPISystemWrapper interface. This interface has only one method called GetSnapshotValuesFromPoints with one string input. This function finds all PI Points filtered using the pointNameFilter input and then finds all of its snapshots.

 

\Framework\IPISystemWrapper.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public interface IPISystemWrapper
{
Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter);
}
}

 

The AfSdkWrapper class implements the IPISystemWrapper interface. This is the only class that has PI AF SDK under this VS project. This class implements GetSnapshotValuesFromPoints method using PI AF SDK code in order to show to the user the requested snapshots.

 

\Framework\AfSdkWrapper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.PI;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public class AfSdkWrapper : IPISystemWrapper
{
private PIServer piServer;

public AfSdkWrapper()
{
//On production, please store the PI Data Archive name on appsettings.json
this.piServer = new PIServers()["MARC-PI2016"];
piServer.Connect();
}
public async Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter)
{
PIPointQuery query = new PIPointQuery(PICommonPointAttributes.Tag, OSIsoft.AF.Search.AFSearchOperator.Equal, pointNameFilter);
IEnumerable<PIPoint> foundPoints = PIPoint.FindPIPoints(piServer, new PIPointQuery[] { query });
PIPointList pointList = new PIPointList(foundPoints.Take(1000));


List<PIPointSnapshotModelView> pointSnapshotValueList = new List<PIPointSnapshotModelView>();
AFListResults<PIPoint, AFValue> values = await pointList.EndOfStreamAsync();
foreach (PIPoint point in pointList)
{
AFValue value = values.Where(v => v.PIPoint == point).Single();
pointSnapshotValueList.Add(new PIPointSnapshotModelView(point.Name.ToString(), value));
}
return pointSnapshotValueList;

}
}
}

 

The HomeController injects the IPISystemWrapper interface which has the unique method needed for the Result action to display the results to the View.

 

\Controllers\HomeController.cs

using ASPNETCoreWithPIAFSDK.Framework;
using ASPNETCoreWithPIAFSDK.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.PI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ASPNETCoreWithPIAFSDK.Controllers
{
public class HomeController : Controller
{
private IPISystemWrapper piSystemWrapper;

//Injecting the IPISystemWrapper service at runtime
// This interface could be AfsdkWrapper or PIWebApiWrapper, according to what is defined on the Startup.cs
public HomeController(IPISystemWrapper piSystemWrapper)
{
this.piSystemWrapper = piSystemWrapper;
}
public IActionResult Index()
{
return this.RedirectToAction("Query");
}

public IActionResult Query()
{
return View();
}

public async Task<IActionResult> Result(string pointNameFilter)
{
IEnumerable<PIPointSnapshotModelView> values = await piSystemWrapper.GetSnapshotValuesFromFoundPoints(pointNameFilter);
return View(values);
}
}
}

 

Finally, the Startup.cs is the file that adds a service of the type IPISystemWrapper with an implementation of this interface which in our case is AfSdkWrapper. In other words, every time the IPISystemWrapper needs to be injected, the DI will inject an instance of the AfSdkWrapper.

 

\Startup.cs

using ASPNETCoreWithPIAFSDK.Framework;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ASPNETCoreWithPIAFSDK
{
public class Startup
{
public IConfiguration Configuration { get; set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPISystemWrapper, AfSdkWrapper>();
services.AddMvc();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
}

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();


}
}
}

 

Running this application, we can see the snapshot values after making a search:

 

 

Replacing PI AF SDK with PI Web API

Finally, I want to demonstrate how easy it is to switch from PI AF SDK to PI Web API using the DI infrastructure. Let's create the PIWebApiWrapper that implements IPISystemWrapper interface. I won't comment about the PI Web API code because it is out of the scope of this blog post.

 

Disclaimer:

Any of the code in this blog could contain bugs and shouldn’t be used in production without extensive testing.

You agree that if you use any of the provided code in your own production code that you accept all ownership, risks, liabilities, and responsibilities associated with the performance, support, and maintenance of the code.

 

\Framework\PIWebApiWrapper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using ASPNETCoreWithPIAFSDK.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace ASPNETCoreWithPIAFSDK.Framework
{
public class PIWebApiWrapper : IPISystemWrapper
{
private HttpClient client;
private HttpClientHandler handler;
private string dataServerWebId;

public PIWebApiWrapper()
{
//On production, please store the PI Web API base URL on appsettings.json
this.BaseUrl = "https://marc-web-sql.marc.net/piwebapi";
this.UseKerberos = true;
var handler = new HttpClientHandler();
handler.UseDefaultCredentials = true;
client = new HttpClient(handler);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("X-Requested-With", "PIWebApiWrapper");


}

public string BaseUrl { get; private set; }
public string Username { get; private set; }
public string Password { get; private set; }
public bool UseKerberos { get; private set; }

public async Task<IEnumerable<PIPointSnapshotModelView>> GetSnapshotValuesFromFoundPoints(string pointNameFilter)
{
List<PIPointSnapshotModelView> pointSnapshotList = new List<PIPointSnapshotModelView>();

if (string.IsNullOrEmpty(this.dataServerWebId) == true)
{
//On production, please store the PI Data Archive name on appsettings.json
string url = this.BaseUrl + @"/dataservers?path=\\MARC-PI2016";
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
HttpResponseMessage httpResponse = await client.SendAsync(httpRequest);
string response = await httpResponse.Content.ReadAsStringAsync();
JObject jObject = JsonConvert.DeserializeObject<JObject>(response);
this.dataServerWebId = jObject["WebId"].ToString();
}

string url2 = this.BaseUrl + "/dataservers/" + this.dataServerWebId + "/points?nameFilter=" + pointNameFilter;
HttpRequestMessage httpRequest2 = new HttpRequestMessage(HttpMethod.Get, url2);
HttpResponseMessage httpResponse2 = await client.SendAsync(httpRequest2);
string response2 = await httpResponse2.Content.ReadAsStringAsync();


JObject jObject2 = JsonConvert.DeserializeObject<JObject>(response2);

string url3 = this.BaseUrl + "/streamsets/value?";
List<Tuple<string, string>> list = new List<Tuple<string, string>>();
if (jObject2["Items"] != null)
{
foreach (var item in jObject2["Items"])
{
string webId = item["WebId"].ToString();
string name = item["Name"].ToString();
Tuple<string, string> tuple = new Tuple<string, string>(name, webId);
list.Add(tuple);
url3 = url3 + $"webId={webId}&";
}
url3 = url3.Substring(0, url3.Length - 1);
}
else
{
return pointSnapshotList;
}


HttpRequestMessage httpRequest3 = new HttpRequestMessage(HttpMethod.Get, url3);
HttpResponseMessage httpResponse3 = await client.SendAsync(httpRequest3);
string response3 = await httpResponse3.Content.ReadAsStringAsync();

JObject jObject3 = JsonConvert.DeserializeObject<JObject>(response3);
if (jObject3["Items"] != null)
{
foreach (var item in jObject3["Items"])
{
string webId = item["WebId"].ToString();
Tuple<string, string> tuple = list.Where(p => p.Item2 == webId).Single();
PIPointSnapshotModelView piPointSnapshotModelView = new PIPointSnapshotModelView(tuple.Item1);
if (tuple != null)
{
if ((item["Value"]["Value"].Type != JTokenType.Object))
{
piPointSnapshotModelView.Value = item["Value"]["Value"].ToString();
}
else
{
piPointSnapshotModelView.Value = item["Value"]["Value"]["Name"].ToString();
}
piPointSnapshotModelView.TimeStamp = Convert.ToDateTime(item["Value"]["Timestamp"].ToString());
}
pointSnapshotList.Add(piPointSnapshotModelView);
}
}
return pointSnapshotList;
}
}
}

 

If we update the ConfigureServices method, the application will use PI Web API instead of PI AF SDK and present the same results to the final user.

 

\Startup.cs

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPISystemWrapper, PIWebApiWrapper>();
services.AddMvc();
}

 

Conclusions

Using Dependency Injenction on your PI applications on top of ASP.NET Core is very interesting approach to help maintain, organize and test your your app for many years. It is not the easiest concept to understand but you will quickly see its value once you start implement it.

Outcomes