Introduction to the new Web API for Deltek Vantagepoint

This is a transcript of my Deltek Vantagepoint REST API session of the same name at 2018’s Deltek Insight.

Agenda

  • Introduction
  • Connecting to the REST API
  • Available Methods
  • Reading Data
  • Writing Data
  • Working with JSON
  • Where to take it from here
  • Tips & Tricks

Introduction

With the move to Vantagepoint, the API technology changes from SOAP to REST

For the time being the old SOAP visionws.asmx services are supported (to an extend)

You will have to move any old Vision API code to the new VantagePoint REST calls sooner or later

New API can be used from many different clients (Jscript, PHP, Ruby, …)

Still a work in progress, new features are added constantly

What is REST

The acronym REST stands for Representational State Transfer

Each unique URL is a representation of some object.

REST uses the standard Http “verbs”

  • GET: retrieve the contents of the requested object (SELECT)
  • POST: send an entity to a URI (UPDATE, INSERT)
  • PUT: store an entity at a URI (INSERT)
  • DELETE: request an entity to be removed

Connecting To The REST API

Authentication in Vantagepoint

The authentication in Vantagepoint is token based

Every REST API endpoint you access requires that you supply an access token in the header of your request to verify that you are an authorized user.

Authorization involves the following three steps:

  • Generate a client secret based on your client ID.
  • Use the secret to get an access token.
  • Include the access token in all API requests

Generate a Secret

Your application identifies itself to Vantagepoint by using the secret, which is a unique code that associates with your client ID. You only need to generate the secret once.

To generate the secret:

  • In the Navigation pane, select Utilities » Integrations » API Authorization.
  • Click Generate Secret.
  • Store the information for your application

Create a Http Client

All communications to the REST API will use a standard Http Client

This code shows how to set it up with the basic information needed to communicate with Vantagepoint

public HttpClient GetVantagePointClient()
{
     //set up client
     HttpClient client = new HttpClient();
     client.BaseAddress = new Uri(Settings.BaseURL);
     client.DefaultRequestHeaders.Accept.Clear();
     client.DefaultRequestHeaders.Accept.Add(
     new MediaTypeWithQualityHeaderValue("application/json"));
     return client;
}

Get the Access Token

You have to submit a Http POST request to the api/token endpoint to obtain the access token:

public async Task<TokenInfo> GetVantagePointTokenAsync(HttpClient client)
{
     Dictionary<string, string> values = new Dictionary<string, string>();
     values.Add("username", Settings.Username);
     values.Add("password", Settings.Password);
     values.Add("grant_type", "password");
     values.Add("integrated", "N");
     values.Add("database", Settings.Database);
     values.Add("client_Id", Settings.ClientId);
     values.Add("client_secret", Settings.ClientSecret);

     var content = new FormUrlEncodedContent(values);

     HttpResponseMessage response = await client.PostAsync("token", content);
     if (response.IsSuccessStatusCode)
     {
          string json = await response.Content.ReadAsStringAsync();
          TokenInfo token = JsonConvert.DeserializeObject<TokenInfo>(json);

          return token;
     }

     return null;
}

Use an Authorized Client

Once you have received a token by the application, use the token to make repeat calls to the REST API.

The token is submitted as part of the authentication header of the Http Client

Your code should check for an expired token and then refresh it when necessary instead of always requesting a new one

public async Task<HttpClient> GetAuthorizedVantagePointClientAsync()
{
     HttpClient client = GetVantagePointClient();

     //for simplicity sake we ALWAYS request a new token each time you request a 
     //new authorized client. Instead here you should check if there is an existing 
     //token (retrieve from some settings) and check its validity.
     //If the token expired, request a refreshed token via RefreshTokenAsync
            
     //TokenInfo token = GetFromSettings();
     //token = await RefreshTokenAsync(token);

     TokenInfo token = await GetVantagePointTokenAsync(client);
     client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
                "Bearer", token.access_token);

     return client;
}

Refresh Token

When refreshing your token always send in the specific “refresh_token” you received with the initial call to the token provider.

Use the newly returned bearer token and refresh token for all subsequent calls

public async Task<TokenInfo> RefreshTokenAsync(HttpClient client, TokenInfo currentToken)
{
     Dictionary<string, string> values = new Dictionary<string, string>();
     values.Add("refresh_token", currentToken.refresh_token);
     values.Add("grant_type", "refresh_token");
     values.Add("client_Id", Settings.ClientId);
     values.Add("client_secret", Settings.ClientSecret);

     var content = new FormUrlEncodedContent(values);

     HttpResponseMessage response = await client.PostAsync("token", content);
     if (response.IsSuccessStatusCode)
     {
          string json = await response.Content.ReadAsStringAsync();
          TokenInfo token = JsonConvert.DeserializeObject<TokenInfo>(json);

          return token;
      }

      return null;
}

Reading Data

Sending a GET to the API

GET information is usually submitted in the URI (the web address) itself and can look like this (page breaks for easier readability:

http://localhost/Storm/vision/project/
	?order=name
	&lookuptype=wbs1
	&searchType=ALL
	&pagesize=100
	&offset=0
	&page=1
	&wbstype=WBS1
	&isLevelLock=false
	&fieldFilter=WBS1%2CName%2CClientName%2CProjMgr%2COrg%2CStatus

Reading Project Data

Get an authorized API client (with bearer token)

Put together the GET request URI

Send the GET URI to the API and retrieve the HttpResponseMessage

Process the response and extract the entities sent by the API

public async Task<ActionResult> TopTen()
{
     var authClient = await _repository.GetAuthorizedVantagePointClientAsync();

     //set up the query string to return projects:
     string requestUri = $"project?limit=10";
     //only display top level projects
     requestUri += $"&wbstype=wbs1";
     //add a fieldFilter to the query string so the API only returns fields that are needed            
     requestUri += $"&{RESTHelper.GetFieldFilterParamString(new string[] {"WBS1", "Name", "LongName" })}";
     //add a search to it
     List<Helpers.FilterHash> searchItems = new List<FilterHash>() {
          new Helpers.FilterHash() {name="ChargeType", value="R",
          tablename ="PR", opp="=", searchlevel=1 },   //regular projects only
          new Helpers.FilterHash() {name="ProjectType", value="07",
          tablename ="PR", opp="=", searchlevel=1 }  //only items with project type 07
     };
     requestUri += RESTHelper.GetSearchFilterParamString(searchItems);

     //call with dynamic type (returns JObject)
     var TopTenList = await _repository.GetAsync(authClient, requestUri);

     //if you build a model that matches the expected result then it can be cast automatically
     var TopTenListTyped = await 
          _repository.GetAsync<List<Models.ProjectModelView>>(authClient, requestUri);

     return View(TopTenListTyped);
}

GET Code explained

  • https://{yourserver}/api/project: this is the base URI for project information
  • ?limit=10: limits the returned result set to the first 10 entries
  • &wbstype=wbs1: only return the top level project
  • &fieldfilter=WBS1%2CName%2CLongName: only return WBS1, Name and LongName
  • &filterhash[i][…]=somevalue: creates a where clause in the API. All filterhash items with the same index belong together. There are multiple available properties to create the filter. The code contains a helper method to help you with this.

GET Response

The response contains a JSON array with all matching projects. You can work directly with the JSON text or cast it into a matching class object

[
	{
		"WBS3": " ",
		"WBS2": " ",
		"LongName": "Albert Ballfour Cole Plaza Study",
		"Name": "ABC Plaza Study",
		"WBS1": "1999009.00"
	}, 
	{…}
]

Writing Data

Sending a POST to the API

POST submits the entity information as part of its message content (not in the URI)

The API uses JSON (javaScript Object Notation) as content type

Our team uses Newtonsoft.Json to serialize and deserialize objects into that content type

Reading Project Data

»Get an authorized API client (with bearer token) »Put together the POST request URI »Create a class object with the entity information you want to send »Post the information to the API »Process the response and check if it was successful

[HttpPost]
public async Task<IActionResult> Contact(Models.ContactUsModel model)
{
     var authClient = await _repository.GetAuthorizedVantagePointClientAsync();

     //set up the query string to post contacts:
     string requestUri = $"contact";

     //this only works because the model and the contact class in VantagePoint
     //share the same names!
     var result = await _repository.PostAsync(authClient, requestUri, model);
            
     return RedirectToAction("ThankYou", model);
}

POST code explained

The application retrieves a view model back from the call that matches the Contacts definition in Vantagepoint. If your model does not match you will need to create the JSON information manually

https://{yourserver}/api/contact: this is the base URI for contact information

The PostAsync method deserializes the object into JSON and adds it as content to the POST message sent to the API

POST Response

The POST response contains the full JSON contact object

[
	{
		"Company": "",
		"flName": "Michael Dobler",
		"ContactID": "829541130BB249759346BCEBEE87706D",
		"ClientID": "",
		"CLAddress": "",
		"Vendor": "",
		…,
	}
]

Executing a Stored Procedure

Sending a POST to the API

To execute a custom stored procedure from the REST API, the stored procedure name must start with “DeltekStoredProc_

You pass in the stored procedure name excluding the “DeltekStoredProc_” portion by calling a post to https://{yourserver}/api/Utilities/InvokeCustom/{storedprocname}

Parameters are passed in as a Dictionary object in the content of the POST

If you return data from that stored procedure it will be sent as an XML document

Sample Code

The sample code will call a stored procedure DeltekStoredProc_GetInvoiceInfo which will return the main invoice info in the first structure and the section totals in the second structure

There is a helper function that turns the returned XML structure in either a JSON object or a list of dictionary objects.

From there the view model is populated based on the (in this case) dictionary object.

Sample Output

A very simple web page to display the invoice section details

Enter an existing invoice number (including leading zeros)

The stored procedure will return two result sets: the first with one row and all the main invoice info

The second result set has the section code and total amount per section code

Sample Code Explained

Calling the stored procedure: provide the custom part of the stored procedure as part of the request URI

Create a dictionary with all parameters of the stored procedure and pass it as content to the POST action

Once it’s been transformed you can use one to populate your view model.

[HttpPost]
public async Task<ActionResult> Index(Models.InvoiceViewModel model)
{
     var authClient = await _repository.GetAuthorizedVantagePointClientAsync();

     string requestUri = $"utilities/invokecustom/getinvoiceinfo";
     Dictionary<string, object> spParams = new Dictionary<string, object>();
     spParams.Add("Invoice", model.RequestInvoice);

     //returns a structure in xml
     string invoiceInfo = await _repository.PostAsync<string>(authClient, requestUri, spParams);

     //turn structure into json
     var retvalJson = Helpers.XMLHelpers.StoredProcXMLToJObject(invoiceInfo);

     //turn structure into dicts
     var retvalDict = Helpers.XMLHelpers.StoredProcXMLToDictionary(invoiceInfo);

     //populating the model the hard way
     //with this code you have to know exactly what the stored procedure returns...
     model.Invoice = retvalDict["Table"][0]["Invoice"].ToString();
     model.MainWBS1 = retvalDict["Table"][0]["MainWBS1"].ToString();
     model.InvoiceDate = DateTime.Parse(retvalDict["Table"][0]["InvoiceDate"].ToString());
     model.MainName = retvalDict["Table"][0]["MainName"].ToString();
     model.Description = retvalDict["Table"][0]["Description"].ToString();
     model.ProjectName = retvalDict["Table"][0]["ProjectName"].ToString();
     model.ClientName = retvalDict["Table"][0]["ClientName"].ToString();

     foreach (var item in retvalDict["Table1"])
     {
          Models.InvoiceSectionViewModel section = new Models.InvoiceSectionViewModel();
          section.Section = item["section"].ToString();
          section.BaseAmount = Decimal.Parse(item["BaseAmount"].ToString());
          section.FinalAmount = Decimal.Parse(item["FinalAmount"].ToString());
          model.Sections.Add(section);
     }

     model.TotalInvoiceAmount = model.Sections.Sum(x => x.FinalAmount);

     return View(model);
}

Parsing the returned XML into JSON

public static JObject StoredProcXMLToJObject(string xml)
{
	XDocument xmlContent;

	try
	{
		xmlContent = XDocument.Load(new System.IO.StringReader(xml));
	}
	catch (Exception)
	{
		return new JObject();
	}

	if (xmlContent.ElementExists("NewDataSet"))
	{
		JTokenWriter jWriter = new JTokenWriter();
		jWriter.WriteStartObject();
		int i = 0;
		string tableName = "Table";
		while (xmlContent.Element("NewDataSet").ElementExists(tableName))
		{
			//write a property for each table
			jWriter.WritePropertyName(tableName);
			//write an array for each collection of table entities
			jWriter.WriteStartArray();

			foreach (XElement item in xmlContent.Element("NewDataSet").Elements(tableName))
			{
				jWriter.WriteStartObject();
				foreach (XElement prop in item.Elements())
				{
					jWriter.WritePropertyName(prop.Name.LocalName);
					jWriter.WriteValue(prop.Value);
				}
				jWriter.WriteEndObject();
			}
			jWriter.WriteEndArray();

			i++;
			tableName = $"Table{i}";
		}

		jWriter.WriteEndObject();

		return (JObject)jWriter.Token;
	}

	return new JObject();
}

Parsing the returned XML into a Dictionary

public static Dictionary<string, List<Dictionary<string, object>>> 
            StoredProcXMLToDictionary(string xml)
{
	var dict = new Dictionary<string, List<Dictionary<string, object>>>();

	XDocument xmlContent;

	try
	{
		xmlContent = XDocument.Load(new System.IO.StringReader(xml));
	}
	catch (Exception)
	{
		return dict;
	}

	if (xmlContent.ElementExists("NewDataSet"))
	{
		int i = 0;
		string tableName = "Table";

		//table level
		while (xmlContent.Element("NewDataSet").ElementExists(tableName))
		{
			//row level
			var rows = new List<Dictionary<string, object>>();
			foreach (XElement item in xmlContent.Element("NewDataSet").Elements(tableName))
			{
				Dictionary<string, object> props = new Dictionary<string, object>();
				foreach (XElement prop in item.Elements())
				{
					//item level
					props.Add(prop.Name.LocalName, prop.Value);
				}
				rows.Add(props);
			}

			dict.Add(tableName, rows);

			i++;
			tableName = $"Table{i}";
		}
	}

	return dict;
}

Available Methods

Online Help

You can find full Deltek Vantagepoint 2.0 API reference here: https://api.deltek.com/Product/Vantagepoint/api/

It comes with detailed documentation and code samples in different languages

Where to take it from here

Build your own integrations!

With the available documentation and the sample code there are plenty of scenarios where you can use the new functionality in your company

REST will allow you to pull data directly into your web site via jScript.

Automated lead generation, pull prestige projects directly from Vantagepoint into your website, push data from an Excel sheet into projects, …

Additional Resources

Links and Downloads

All demos use the Vision/VantagePoint demo database which can be downloaded from the Deltek Support Site

You can find all source code on GitHub: https://github.com/mdobler/Insight2018

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.