At LeanKit we’re always creating new integrations with other software and sites. Most of them involve reading from a 3rd party application’s web service API (Microsoft’s TFS via VisualStudio.com, JIRA, GitHub issues and more), performing some type of translation and mapping of the data, and then writing to LeanKit’s web service API to manipulate (add, update, remove) cards on a board. Some integrations also read from LeanKit and write to the target application to provide 2-way syncing of data.
Cards on a LeanKit board that were imported from Microsoft’s Project Online by a node application using Edge.js to communicate with Microsoft Projects CSOM API dlls.
Recently we have been working with Node.js to offer a “run anywhere” option for our integration services. Using Node has been great when communicating with most web service APIs, especially those that are REST-based but even those that are SOAP-based are not too horrible. But in cases where I am forced to consume WCF web services I’d much rather use a client-side API if one is available. Working with Microsoft Project Server 2013 and specifically Project Online in addition to SharePoint because Project Server is built on SharePoint is one of those cases. SharePoint and Project Server have a CSOM (Client Side Object Model) API that works pretty well. However, the problem for our scenario is those APIs are a pair of .NET dlls which do not compute in our Node environment. Yes, I know there is a SharePoint/Project JSOM (Javascript Object Model) API that might work in our scenario but in my limited experience with it I found it to be… well… to use a technical term: “weird”… and I did not care for it.
We want to get tasks from a project hosted on Microsoft Project Online. We have a lot of Javascript code written for Node for handling that data once we get it. We have .NET dlls that provide an API for working with Microsoft Project Online. So how can we tie the two together? How can our Node application communicate with a .NET library?
Well we could create a web service interface to act as a facade for communicating with the library and consume that in our Node application. That’s a decent option and one we considered. Instead we decided to take a look at Edge.js.
What is Edge.js? Pure .NET & Node awesomeness. According to the introduction: Edge.js connects Node.js and .NET ecosystems … on Windows, MacOS, and Linux in one process.
I first learned about Node.js from my super smart friend and co-worker David Neal: How to leverage SQL Server with Node.js using Edge.js
In layman’s terms: Edge.js allows you to run .NET code in the Node process. Yes, .NET code inside Node! It obviously works on Windows but will also work on MacOS and Linux using Mono as long as the underlying .NET code you are using will run on Mono. In our case the CSOM library we are using will only work on Windows.
Other than my brief explanation above and the examples below I will not provide detailed information about Edge.js, both David’s article and the Edge.js documentation offer plenty of introduction material, quick-start examples, etc. I’ll assume that since you are reading this that you either have some experience with Node or at least some interest so I will not provide details for installing Node or setting up your Node environment, NPM (Node Package Manager), etc.
I don’t need to expose everything that the CSOM API can do to my Node application. I only need to perform a few operations like getting a list of projects and getting a list of tasks for a project. In real life I need to do a few other things like attempt to authenticate a user, add a task, update a task, get a list of team members and more but getting projects and tasks should work well for this demo. To simplify code navigation, readability and testing we’ll create a class, ProjectServerClient, to serve as a wrapper to the CSOM API that will only provide the functionality we need. getting projects and tasks. If you have worked with the SharePoint/Project CSOM before then the below code will look somewhat familiar. I fully admit to not being a SharePoint developer so there may be much better ways to do what I’ve done in the example code. Feel free to offer helpful insight, tips and suggestions.
Some things to keep in mind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.ProjectServer.Client;
using Microsoft.SharePoint.Client;
namespace leankit.integration.client.microsoft.projectserver
{
public class ProjectServerAuth
{
public string Url { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
public class ProjectServerClient
{
public static List<Project> GetProjects(ProjectServerAuth auth)
{
var projects = new List<Project>();
using (ProjectContext projContext = new ProjectContext(auth.Url))
{
projContext.Credentials = new SharePointOnlineCredentials(auth.Username, auth.Password.ToSecureString());
projContext.Load(projContext.Web);
projContext.ExecuteQuery();
var publishedProjects = projContext.LoadQuery(projContext.Projects);
projContext.ExecuteQuery();
foreach (PublishedProject pubProj in publishedProjects)
{
projects.Add(MapToProject(pubProj));
}
}
return projects;
}
public static List<Item> GetTasks(ProjectServerAuth auth, Guid projectId)
{
var items = new List<Item>();
using (ProjectContext projContext = new ProjectContext(auth.Url))
{
projContext.Credentials = new SharePointOnlineCredentials(auth.Username, auth.Password.ToSecureString());
projContext.Load(projContext.Web);
projContext.ExecuteQuery();
var projects = projContext.LoadQuery(projContext.Projects);
projContext.ExecuteQuery();
var project = projects.FirstOrDefault(x => x.Id == projectId);
if (project != null)
{
var ts = project.Tasks;
projContext.Load(ts);
projContext.ExecuteQuery();
var tasks =
projContext.LoadQuery(
ts.IncludeWithDefaultProperties(task =>
task.Assignments.IncludeWithDefaultProperties(assignment => assignment.Resource))
.Where(t => t.IsActive)));
projContext.ExecuteQuery();
foreach (var task in tasks)
{
items.Add(MapToItem(task));
}
}
}
return items;
}
}
}
Now that we have a simpler .NET library to work with it needs to have an interface that Edge.js understands. We could convert all of our methods to Async/Await but I will leave them as-is for code reuse purposes and instead create a wrapper for the wrapper, EdgeProjectServerClient.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Threading.Tasks;
namespace integration.client.projectserver
{
public class EdgeProjectServerClient
{
public async Task<object\> GetProjects(IDictionary<string, object\> input)
{
var auth = new ProjectServerAuth();
ExpandoMapper<ProjectServerAuth>.Map((ExpandoObject)input\["auth"\], auth);
return await Task.Run(() => ProjectServerClient.GetProjects(auth));
}
public async Task<object\> GetTasks(IDictionary<string, object\> input)
{
var auth = new ProjectServerAuth();
ExpandoMapper<ProjectServerAuth>.Map((ExpandoObject)input\["auth"\], auth);
var projectId = Guid.Parse(input\["projectId"\].ToString());
return await Task.Run(() => ProjectServerClient.GetTasks(auth, projectId));
}
}
}
Now we have a couple of methods with signatures that Edge.js can understand. Next we’ll add code to our Node.js app to call into this .NET library.
Let’s create a module that acts as a client library for talking to our .NET code via Edge.js and call it client.js. Our module will simply expose a couple of methods: getProjects and getTasks, which map to the similarly named methods in our .NET library.
var edge = require("edge"),
path = require("path"),
dllpath = path.join(".", 'dlls');
var Client = function(url, user, password) {
var auth = {
"url": url,
"username": user,
"password": password
};
var getProjectsMethod = edge.func({
assemblyFile: path.join( dllpath, 'integration.client.projectserver.dll'),
typeName: 'integration.client.projectserver.EdgeProjectServerClient',
methodName: 'GetProjects'
});
var getTasksMethod = edge.func({
assemblyFile: path.join( dllpath, 'integration.client.projectserver.dll'),
typeName: 'integration.client.projectserver.EdgeProjectServerClient',
methodName: 'GetTasks'
});
var getProjects = function(callback) {
var data = {
auth: auth
};
getProjectsMethod(data, function(err, res) {
if (err || !res){
err = err || 'unknown error';
console.log("err=" \+ err + ", res=" \+ res);
callback(err, null);
} else {
callback(err, res);
}
});
};
var getTasks = function(projectId, callback) {
var data = {
auth: auth,
projectId: projectId
};
getTasksMethod(data, function(err, res) {
if (err || !res){
err = err || 'unknown error';
console.log("err=" \+ err + ", res=" \+ res);
callback(err, null);
} else {
callback(err, res);
}
});
};
return {
getProjects: getProjects,
getTasks: getTasks,
};
};
module.exports = Client;
We simply require the “edge” module and then create a proxy for each .NET method we want to call using Edge’s .func() method, think of it as an anonymous function. The getProjectsMethod and getTasksMethod methods are examples of creating the Node.js proxy to a .NET method by specifying the .NET assembly, class and method name. Later we invoke the .NET methods by invoking the getProjectsMethod and getTasksMethod functions. Note: “dlls” is the name of a folder that is a sibling to the Node src folder. It is where I store the dll for the library that contains the ProjectServerClient and EdgeProjectServerClient classes described above in addition all its dependencies.
Finally we just need to write some code in our index.js file that uses our client module to talk to Project Online. Let’s just get a list of our projects and write the name of each to the console. Assuming we have everything installed and setup properly we only need to start up our Node app and it will print out a list of projects hosted on Project Online.
var Client = require("./client.js");
var client = new Client("https://mysite.sharepoint.com/sites/pwa", "admin", "password");
var projects = client.getProjects(function(err, projects) {
for(var i = 0; i < projects.length; i++) {
console.log("Project: " \+ projects\[i\].Title);
}
});
Running this file via the command line with the command “node [Path To File]/index.js” results in the below output which is a list of all my projects on my Project Online site!
Project: Demo Integration
Project: Test Project 1
Project: Test Project 2
Project: Hello World
Building out additional functionality like getting tasks for each project, getting a list of team members, updating a task, etc. is just a matter of continuing down the same path that we have already started together.
Hopefully you’ll find leveraging your .NET code base with Edge.js as useful and interesting as I have. If you have any questions feel free to give me a shout via the comments below or on Twitter: @danhounshell.