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