- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 09-08-2016 06:40 PM
Updated
11/29/2016: ResourceLink's now can be updated without having to PUT or POST back as a separate String property. A JsonConverter class has been added that returns the resource links value as a reference string which is what Service Now expects when updating or creating a ResourceLink. I have updated this article; recommend retrieving through the git repo here: https://github.com/merccat/ServiceNowRESTClient
Greetings All,
Our organization had prior to the release of the REST API made fairly extensive use of the SOAP API. Unfortunately (I mean fortunately) those have continued working great so I have not had an opportunity to sit down and work with the REST API. This week the opportunity finally came up so I decided to develop a .Net client to hopefully make working the TableAPI in .Net dead simple. My goal was a client that was not tied to any particular integration project and would be easy to move between projects. Hopefully this can be of some use to someone else.
In this article I plan to provide the following in this order:
- Overview, Constructors, Methods & Example.
- Code for all of the core components with descriptions of what is happening.
- A link to a completed class library project that can be customized if needed compiled and included in your integration solution. I may try putting it on a Git Repo but have not really decided yet how I'm going to make the project available.
What I would like back:
- Suggestions for improvements (this is my first real crack at it and I'm sure it can be improved).
Disclaimer:
- We have tested this in our environment, however, this comes with no guarantees of any kind. Make sure the user you are using has appropriate access levels to be able to accomplish what you want while protecting the integrity of your system. Always test, re-test and test some more in non-production environments first.
Part 1: Overview and Usage
Overview
The ServiceNow TableAPI Client is designed to provide access to your ServiceNow instance's data through ServiceNow's REST TableAPI in a .Net application. It is written in C#, however once compiled and referenced as a DLL, you could certainly use it in a VB project as well if you were so inclined. JSON Serialization / DeSerialization is done with the Newtonsoft.JSON package and I used System.Net.WebClient for the actual communication.
There are only 4 classes including the client itself. Those classes are ServiceNowRecord, ServiceNowLink, TableAPIClient and RESTResponse.
- Record (Abstract) - All record types your implementation passes between the client will need to inherit from this base class. Types derived from ServiceNowRecord will simply contain a list of fields which you want to manipulate with the selected table. The properties much match the field names in ServiceNow. sys_id is already included.
- ResourceLink - A pre-built representation of a standard Link as returned by Service Now that can be included as a property in your record type.
- RESTResponse<T> - All methods that have a response will package the response in a RestReponse where T is your record type. It is packaged this way so that errors returned by service now can be included separately. NOTE: This is actually a base class, from which there are two classes you'll actually use derived:
- RESTQueryResponse<T> - The response property is a List<T>
- RESTSingleResponse<T> - The response property is simply T.
- TableAPIClient<T> (where T is a ServiceNowRecord) - The actual client. On initialization it uses reflection to build a list of fields for the web request query string which is part of why the field names in your Class derived from ServiceNowRecord need to use the actual field names.
Client Constructors
Signature | Description |
---|---|
TableAPIClient(String tableName, String instanceName, NetworkCredential credentails) | Table name is the name of the table in service now to be accessed Instance name is the name of your service now instances. ie instanceName.service-now.com Credentials is a set of network credentials (with username/pwd) for the user to be used. |
TableAPIClient(String tableName, String instanceName, String userName, String pwd) | Same as above but simple strings for username and password instead of NetworkCredential. |
Public Methods
Name | Description |
---|---|
RestSingleResponse<T> GetById (String id) | Retrieves a single record contained in the RESTSingleResponse of the type T defined for the table client. Any errors will be fully captured and returned in the ErrorMsg property of the response. id is the sys_id of the record to be retrieved. |
RestQueryResponse<T> GetByQuery (String query) | Retrieves a record set in the response (as a list of T) from the query result (if successful) along with any error messages (if any). query is a standard service-now table query. |
RestSingleResponse<T> Post(T record) | Creates a new record in the table using the fields provided in the record of type T. A RestResponse containing a single result of T (if successful) for your newly created record, along with any error messages (if any). record is the record of type T to be added to the table in ServiceNow. |
RestSingleResponse<T> Put(T record) | Updates an existing record in the table. Make sure your record has sys_id populated. All fields of T will be sent in the update so make sure they are ALL populated unless you want the field to be emptied. A good practice is to first retrieve the record using GetById, then only edit the fields you want to change before submitting a Put. A RestResponse containing a single result of T with your updated record (if successfull) along with any error messages (if any). record is the record of type T to be added to the table in ServiceNow. |
Delete(String id) | Deletes a record in the active table with the specified id. WARNING: Since there is no return type, you will need to catch any errors externally. The exception message will still include all details including any response from Service Now. |
Example
I tried to make the actual usage of the client as simple and straight forward as possible. The following code is of course assuming you have already included the compiled DLL in your project (or are working from the client code project - avoid that). The example will be working with a few fields from the sys_user table.
Step 1:
Create a ServiceNowRecord representing the data structure you want to work with from your table. This can be thought of as our data contract. The example below is a simple example to retrieve some user details. Normally we would of course be working with more fields than this, but lets keep it short and sweet:
using Newtonsoft.Json;
{
public class ServiceNowUser : Record
{
[JsonProperty("employee_number")]
public string employee_number { get; set; }
[JsonProperty("first_name")]
public string first_name { get; set; }
[JsonProperty("last_name")]
public string last_name { get; set; }
}
}
Notice how there is no mention of the table name here, I'm only concerned about the fields. Also, sys_id isn't included because the base ServiceNowRecord already includes that.
Also, note that currently all properties are returned as strings. Why? Well, because everything in a JSON string is a string until you parse it. I'm sure you can add parsers to your record type if needed, but for this case none are needed since everything is a string anyway.
Step 2:
Instantiate my client using the record type created above. Note when instantiating the client I will also need to provide my instance name, the table name I want to work with and a set of credentials (either as username and password or System.Net.NetworkCredential).
TableAPIClient<ServiceNowUser> UserClient = new TableAPIClient<ServiceNowUser>("sys_user", "intance_name", "api_username", "password");
Step 3:
Invoke a method. Lets get a record (assuming we know it's sys_id):
var response = UserClient.GetById("7648c1904a36231701673929d43fd325");
if(!response.IsError) { ServiceNowUser myUser = response.result; }
or for another example, we can query for all active users in a given department (agency) using a standard service now query:
Users = new List<ServiceNowUser>();
string query = "u_agency.sys_id=e2924ee14a3623170084831c4a6b5b9c^active=true";
var result = UserClient.GetByQuery(query);
if (!result.IsError) { Users = result.result; }
Part 2: Core Code
The following code is not an example of usage, it is the code of the client itself along with key supporting classes.
Record
This is an abstract class used to build classes which describe the fields to be retrieved (or updated) from a given table in ServiceNow.
public abstract class Record
{
[JsonProperty("sys_id")]
public string sys_id { get; set; }
// Prevent sys_id from being serialized when sending to ServiceNow while still allowing it to be de-serialized in the response
public bool ShouldSerializesys_id() { return false; }
}
ResourceLink
Related records are returned by link. Since I do frequently retrieve related records, I find this class to be useful, although it's not technically part of the core, it will be included in the posted solution. An example of usage will also be provided.
public class ResourceLink
{
[JsonProperty("link")]
public string link { get; set;} // REST URL for the child record
[JsonProperty("value")]
public string value { get; set; } // sys_id of the child record.
public override string ToString() { return value; }
}
ResourceLinkConverter
Since Service Now requires related objects (returned as a ResourceLink) to be updated as a string reference to the related entity (such as sys_id), this class will on Serialization for transmission back to Service Now, convert objects of type ResourceLink to a simple string value using the ToString method of the ReourceLink.
public class ResourceLinkConverter : JsonConverter
{
public override bool CanRead { get { return false; } }
public override bool CanWrite { get { return base.CanWrite; } }
public override bool CanConvert(Type objectType) { return objectType == typeof(ResourceLink); }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if(t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JToken sys_id = JToken.FromObject(value.ToString());
sys_id.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// We have set this to not operate on Read
throw new NotImplementedException();
}
}
RESTResponse
This next code block will have the 3 RESTResponse classes. First the abstract class used to build the two response types which are used, followed by RESTQueryResponse<T> and RESTSingleResponse<T>.
public abstract class RestResponse
{
public String RawJSON { get; set; }
public String ErrorMsg { get; set; }
public RestResponse()
{
this.RawJSON = "";
this.ErrorMsg = "";
}
public bool IsError
{
get
{
if(ErrorMsg.Length > 0) { return true; }
return false;
}
}
}
public class RESTQueryResponse<T> : RestResponse
{
public RESTQueryResponse()
{
this.result = new List<T>();
}
public List<T> result { get; set; }
}
public class RESTSingleResponse<T> : RestResponse
{
public RESTSingleResponse() { }
public T result { get; set; }
}
TableAPIClient
Finally, the client itself. Because we make use of an existing web client (System.Net.WebClient) and an existing deserializer (Newtonsoft.Json) the code here is actually fairly small. In fact the bulk of the lines of code are simply error handling.
public class TableAPIClient<T> : ServiceNow.Interface.ITableAPIClient<T> where T : Record
{
private String _TableName;
private String _InstanceName;
private String _FieldList;
private WebClient ServiceNowClient;
public TableAPIClient(String tableName, String instanceName, NetworkCredential credentials)
{
initialize(tableName, instanceName, credentials);
}
public TableAPIClient(String tableName, String instanceName, String userName, string password)
{
NetworkCredential credentials = new NetworkCredential { UserName = userName, Password = password };
initialize(tableName, instanceName, credentials);
}
private void initialize(String tableName, String instanceName, NetworkCredential credentials)
{
_TableName = tableName;
_InstanceName = instanceName;
// Initialize the Web Client
ServiceNowClient = new WebClient();
ServiceNowClient.Credentials = credentials;
// Build the field list from the record type that will be retrieved
_FieldList = "";
Type i = typeof(T);
foreach (var prop in i.GetProperties())
{
// We need to build the field list using the JsonProperty attributes since those strings can contain our dot notation.
var field = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "JsonPropertyAttribute");
if (field != null)
{
var fieldName = field.ConstructorArguments.FirstOrDefault(x => x.ArgumentType.Name == "String");
if (fieldName != null)
{
if (_FieldList.Length > 0) { _FieldList += ","; }
_FieldList += fieldName.Value;
}
}
}
}
private String URL { get { return "https://" + _InstanceName + ".service-now.com/api/now/table/" + _TableName; } }
private string ParseWebException(WebException ex)
{
string message = ex.Message + "\n\n";
if (ex.Response != null)
{
var resp = new StreamReader(ex.Response.GetResponseStream()).ReadToEnd();
dynamic obj = JsonConvert.DeserializeObject(resp);
message = "status: " + obj.status + "\n";
message += ex.Message + "\n\n";
message += "message: " + obj.error.message + "\n";
message += "detail: " + obj.error.detail + "\n";
}
return message;
}
public RESTSingleResponse<T> GetById(string id)
{
var response = new RESTSingleResponse<T>();
try
{
response.RawJSON = ServiceNowClient.DownloadString(URL + "/" + id + "?&sysparm_fields=" + _FieldList);
}
catch (WebException ex)
{
response.ErrorMsg = ParseWebException(ex);
}
catch (Exception ex)
{
response.ErrorMsg = "An error occured retrieving the REST response: " + ex.Message;
}
RESTSingleResponse<T> tmp = JsonConvert.DeserializeObject<RESTSingleResponse<T>>(response.RawJSON);
if (tmp != null) { response.result = tmp.result; }
return response;
}
public RESTQueryResponse<T> GetByQuery(string query)
{
var response = new RESTQueryResponse<T>();
try
{
response.RawJSON = ServiceNowClient.DownloadString(URL + "?&sysparm_fields=" + _FieldList + "&sysparm_query=" + query);
}
catch (WebException ex)
{
response.ErrorMsg = ParseWebException(ex);
}
catch (Exception ex)
{
response.ErrorMsg = "An error occured retrieving the REST response: " + ex.Message;
}
RESTQueryResponse<T> tmp = JsonConvert.DeserializeObject<RESTQueryResponse<T>>(response.RawJSON);
if (tmp != null) { response.result = tmp.result; }
return response;
}
public RESTSingleResponse<T> Put(T record)
{
var response = new RESTSingleResponse<T>();
try
{
string data = JsonConvert.SerializeObject(record, new ResourceLinkConverter());
response.RawJSON = ServiceNowClient.UploadString(URL + "/" + record.sys_id + "?&sysparm_fields=" + _FieldList, "PUT", data);
}
catch (WebException ex)
{
response.ErrorMsg = ParseWebException(ex);
}
catch (Exception ex)
{
response.ErrorMsg = "An error occured retrieving the REST response: " + ex.Message;
}
RESTSingleResponse<T> tmp = JsonConvert.DeserializeObject<RESTSingleResponse<T>>(response.RawJSON);
if (tmp != null) { response.result = tmp.result; }
return response;
}
public RESTSingleResponse<T> Post(T record)
{
var response = new RESTSingleResponse<T>();
try
{
string data = JsonConvert.SerializeObject(record, new ResourceLinkConverter());
response.RawJSON = ServiceNowClient.UploadString(URL + "?&sysparm_fields=" + _FieldList, "POST", data);
}
catch (WebException ex)
{
response.ErrorMsg = ParseWebException(ex);
}
catch (Exception ex)
{
response.ErrorMsg = "An error occured retrieving the REST response: " + ex.Message;
}
RESTSingleResponse<T> tmp = JsonConvert.DeserializeObject<RESTSingleResponse<T>>(response.RawJSON);
if (tmp != null) { response.result = tmp.result; }
return response;
}
public void Delete(string id)
{
try
{
ServiceNowClient.UploadString(URL + "/" + id, "DELETE", "");
}
catch (WebException ex)
{
string ErrorMsg = ParseWebException(ex);
throw new Exception(ErrorMsg);
}
}
}
Part 3: Completed Solution
I'm still working on a good sample solution, but for now I have put something together here: https://github.com/merccat/ServiceNowRESTClient
GitHub - merccat/ServiceNowRESTClient: REST Client for ServiceNow's TableAPI written in C#
Note that as I was building that repository, I renamed a few of the classes to shorten names and take advantage of the namespaces they are already contained in. For example instead of ServiceNowLink it's ResourceLink in the ServiceNow namespace and instead of ServiceNowRecord is simply Record in the ServiceNow namespace. Once firmed up I will update the above documentation and also post a walk-thru of the solution layout and files here.
In summary, there are 4 projects in the solution:
1. TableAPI - Contains the client and core classes.
2. TableDefinitions - Sample table definitions. Note I have not yet removed all custom fields that would only be applicable to our environment.
3. UnitTests - Feel free to add unit tests for the client for your records, or don't.
4. UsageDemo - Contains a couple examples of usage, including retrieving a more detailed record set which includes ResourceLinks and dot traversed properties.
Thank you for taking the time to look at this. I hope it can be of some use to you and your feedback is appreciated.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Nice article.
Did you end up publishing the project as looks like it would be very helpful with what I am doing at the moment.
Regards
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi George,
Thanks! I hope you're able to use it and it helps you out. I'm already using it for employee location/status updates and task time/work log retrieval.
Yes, in fact last night I did go ahead and publish it to GitHub. At the very end of the article I updated it with a link. It's actually my first time creating a repository there so hopefully it went through Ok.
As I was recreating the solution for GitHub I did update a couple class names as noted there with the link. I may change them back though as I did that at the very end of the night and have not yet gone back through it.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi Ryan
Yes did manage to download from GitHub via zip file last night so will look at it over next few days.
Was good timing as I was just about to look at REST to ServiceNow.
Appreciate the "heads-up" and will provide feedback in due course.
Kind Regards
George
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Got source running all good.
Only area doesn't appear to be returning data is the two ResourceLink fields.
Can't seem to get caller details.
Presume you have used two different techniques for the Caller and Opened_By but I don't seem to see where it is retrieving the destination object details.
Regards
g.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks, nice catch! I found that I can retrieve ResourceLinks for items that are on the extension table incident so for example caller_id works but opened_by which is from the base table task for some reason doesn't get returned. I'm probably naming it wrong in the example so I will continue looking at that.
Also, I noticed that my dot-traversal example wasn't working either. In the past when I was testing it I had manually built the field list, but I found that the auto generated field list was not naming those fields the way that service now wanted it. I have already fixed that and will publish it in a couple minutes with that fix and the sample incident adjusted to use caller_id. Meanwhile I'll also look closer at the opened_by example.
-Ryan
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
OK I figured out what I think may be happening with your opened_by ResourceLink, at least it's what it was for me.... I found that the user I was using for the REST login did not have permissions for opened_by which is why it was not being returned. When I use my regular user (which has full admin rights to our system) it is returned.
I don't want to mess with our permissions tonight as I'm not the primary admin, but I imagine that is most likely the same thing that is occurring for you.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi
All working now with changes.
Didn't have any permission issues as was already using administrator rights.
Made some very slight amendments which includes:
1) Used app.config to hold instance/user/password.
Used following rather than your inline variables
static String _Instance = ReadSetting("Instance"); | |
static String _User = ReadSetting("User"); | |
static String _Password = ReadSetting("Password"); |
Plus this new supporting routine (with reference to System.Configuration and using statement)
private static string ReadSetting(string key)
{
string result = "";
try
{
var appSettings = ConfigurationManager.AppSettings;
result = appSettings[key] ?? "";
}
catch (ConfigurationErrorsException)
{
result = "";
}
return result;
}
2) Include displaying number of records found at start of display and Completed message at end.
In RetrievedByQuery replaced foreach with
incident[] incidentList = client.GetByQuery(query).result.ToArray();
Console.WriteLine(incidentList.Length + " records found\r\n");
foreach (incident r in incidentList) //client.GetByQuery(query).result)
and at end of Main before ReadLine
Console.WriteLine("\n\rCompleted."); |
3) Just a minor typo in UnitTests / TableAPI.cs have mispelt RetreivedUsers, should be RetrievedUsers.
Hope above is self-explanatory but can send files if you need the source - just let me know where to send to.
Thank you for your article, was very helpful and had good techniques.
Regards
g.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks! Great point on using app.config, in a real app that is where I would prefer that as well. I will claim that I left the settings directly in the demo project to keep it straightforward and focused on the client usage, but to be perfectly honest I was just being lazy there . Your idea of adding a record count is a good one, in fact, that could probably be useful in a lot of scenarios so I think I'll go ahead and add it to the response object.
Thanks for all your feedback, and help spotting issues.
Kind Regards,
-Ryan
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thank you for this Article, it has helped me a lot.
How ever I still do face issues when trying to deal with the reference field location in sys_user.
If I put a reference like this:
[JsonProperty("location.name")]
public string Location_name { get; set; }
I can get the Name value of the location field but I get stuck if trying to update the this field and noting happens.
I would appreciate if someone could point me in the right direction here.
Kind Regards
-Thomas
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Awesome, thanks! I'm glad it has been a help.
I'll try a couple things this week regarding updating related resources but here is hypothesis as to why it will not let you update a related resource in the same call:
While we can retrieve reference fields, updating is a bit more complex. Since the referenced field is contained in a separate table and a restful update is only supposed to update a single resource, being a record on a table, we can only update the fields in the source table. In order to update the reference field you would want to make a second call using the referenced Id which you can also get and the referenced field you want to update.
Kind Regards,
-Ryan
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi,
I did a workaround to get the location updated, same thing apply to the Company column and it can for sure be done i n better way but it is a step in the right direction.
once again thanks for putting the post out here.
public RESTSingleResponse<T> PutLocation(string sys_id, string location_name)
{
var response = new RESTSingleResponse<T>();
try
{
response.RawJSON = ServiceNowClient.UploadString(URL + "/" + sys_id + "?&sysparm_fields=location", "PUT", "{\"location\":\"" + location_name + "\"}");
}
catch (WebException ex)
{
response.ErrorMsg = ParseWebException(ex);
}
catch (Exception ex)
{
response.ErrorMsg = "An error occured retrieving the REST response: " + ex.Message;
}
RESTSingleResponse<T> tmp = JsonConvert.DeserializeObject<RESTSingleResponse<T>>(response.RawJSON);
if (tmp != null) { response.Result = tmp.Result; }
return response;
}
Kind Regards
-Thomas
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks! I see what is happening now. You're not actually updating the locations name, you are changing the location to another known location using the name. So it is still just an update of the sys_user table. Internally Service Now will query for that location name and set it if found. There is a danger here in that if not found it will not set it and there will be no warning or error from Service Now. Also, if there are multiple matches on the name I believe it would also fail. You can also use the same method with sys_id which may be more reliable than by name.
You're right though, it doesn't feel quite right. I'm not finding much as far as recommendations for setting related records in ServiceNow's documentation, I might have to submit a question to see what the recommended practice is here, then hopefully it will be something I can update the client to handle more gracefully.
For now, I did wrap your idea up (outside of the client) into a method that query's the name, returns an error if not found or multiple matches found and then sets using sys_id if only one found. So it's a tradeoff... more calls, but at least this won't null out anyone's location field if a bad name is passed in:
public static void UpdateUserLocationByName(string user_sys_id, string location_name)
{
TableAPIClient locationClient = new TableAPIClient("cmn_location", myInstance, instanceUserName, instancePassword);
TableAPIClient<sys_user> userClient = new TableAPIClient<sys_user>("sys_user", myInstance, instanceUserName, instancePassword);
string query = @"name=" + location_name;
IRestQueryResponse locationResult = locationClient.GetByQuery(query);
if (locationResult.ResultCount == 0) throw new Exception(String.Format("No location by the name {0} was found.", location_name));
if (locationResult.ResultCount > 1) throw new Exception(String.Format("Multiple locations found by the name {0}.", location_name));
// We found our location lets update the user
userClient.PutLocation(user_sys_id, locationResult.Result.First().sys_id);
}
For this my location definition only needed name:
public class location : ServiceNow.Record
{
[JsonProperty("name")]
public string name { get; set; }
}
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
So here is the challenge:
If you include a related resource such as location, on Get service now will return a ResourceLink. However, on PUT it want's to update by a string reference (sys_id recommended). So on get location needs to map to a property of type ResourceLink but on put it needs to be a string... it won't even accept a fully populated ResourceLink.... how obnoxious.
Anyway I think this can be accomplished in a more automatic kind of way without a whole lot of effort but will require separating our our table definitions a little more. Although I really like the Json Attributes decorating our table classes directly. Essentially we will need to define a custom Json property resolver and then set it up to resolve the ResourceLink field while ignoring the string field when deserializing and vice versa for serialization.
-Ryan
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thank you Ryan for your updated code and efforts due to this.
In my application I query all locations and put them in a "combobox" to make sure the location is available and I changed from name to sys_id, it works like a charm.
As mentioned before the logic will apply to all sys_user columns that are of type related fields, hopefully SNOW people have some best practice how to do this even if I think your piece of code woks great.
My next step will be to add users to groups 🙂
Kind regards
- Thomas
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks, I'm glad it's appreciated!
I'll be updating the repository and the article when I have some down time this weekend, but here is the solution I found... it was easier than I was initially thinking thanks to Json.Net
My approach was to make it so an updated ResourceLink could be passed back over to Service Now using a JsonConverter. What I like about this is that there is no secondary methods to perform the PUT and there are no secondary properties to key off of.
Basically I did three things:
1. I overwrote the ToString() method on the ResourceLink object so that it would always return the value (sys_id):
In ResourceLink: public override string ToString() { return value; }
Note that I didn't have to do this, but I felt that if I used the ToString() method in my JsonConverter it would allow me some control over the serialization within ResourceLink if anyone ever wanted to there.
2. I Created the ResourceLinkConverter which looks like this:
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ServiceNow.TableAPI
{
public class ResourceLinkConverter : JsonConverter
{
public override bool CanRead { get { return false; } }
public override bool CanWrite { get { return base.CanWrite; } }
public override bool CanConvert(Type objectType) { return objectType == typeof(ResourceLink); }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if(t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JToken sys_id = JToken.FromObject(value.ToString());
sys_id.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// We have set this to not operate on Read
throw new NotImplementedException();
}
}
}
Finally I set the Put method serialization line to use the ResourceLinkConverter:
original: string data = JsonConvert.SerializeObject(record);
new: string data = JsonConvert.SerializeObject(record, new ResourceLinkConverter());
I still want to play around with this a little more as I think these JsonConverters might be useful for some other things... initially I thought about clearing out empty fields from the serialized Json but almost immediately remembered we sometimes want to empty out fields so decided against that idea.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Very interesting and useful article!
Just a problem : when I create a new user with "special" french characters (for example in my first name "André"), the "é" is displayed in ServiceNow with a strange black character.
Problem with encoding, I think. An idea how I can overcome this?
-------------------------------------------------------------------------------------------------------
OOPS, sorry, just find the answer, declare the webclient with encoding. :
new WebClient() { Encoding = Encoding.UTF8 };
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hello there-
I have compiled your files into DLLs and imported them into my VB.Net project. I am able to run a getbyquery and I do get a result set back of the information that I am looking for. In my case, I'm just querying the SN database to get a list of all Windows and Linux servers. When I view the RawJSON I can see all of my objects. Example:
"{""result"":[{""sys_id"":""00040665dbxxxxxx96191e"",""u_recovery_time_achievable"":""720"",""sys_class_name"":""cmdb_ci_linux_server"",""host_name"":""rlserver001""},{""sys_id"":""00ec543d1xxxx66e4bcb6d"",""u_recovery_time_achievable"":""4"",""sys_class_name"":""cmdb_ci_linux_server"",""host_name"":""plserver001""},{""sys_id"":""0105d975dbxxxxx8961998"",""u_recovery_time_achievable"":"""",""sys_class_name"":""cmdb_ci_linux_server"",""host_name"":""tlserver001""},
However when I view the RestQueryResponse.Result, I do not see my information filled in there. It's only in the RawJSON. There are the right amount of objects in the .Result, but for each only the sys_id gets filled in. The other fields I am looking for all end up being blank. Here is a sample of my code to give you some idea of what I'm doing:
objServiceNowTableAPIClient = New TableAPIClient(Of ServiceNowServerRecord)(strServiceNowCMDBServersTableName, strServiceNowInstanceName, strServiceNowUser, strServiceNowPassword)
strServiceNowQuery = "sys_class_name=cmdb_ci_win_server^ORsys_class_name=cmdb_ci_linux_server"
objServiceNowRESTQueryResponse = objServiceNowTableAPIClient.GetByQuery(strServiceNowQuery)
Public Class ServiceNowServerRecord
Inherits Record
<Newtonsoft.Json.JsonProperty("sys_class_name")>
Public Property sys_class_name As String
Get
End Get
Set(value As String)
End Set
End Property
<Newtonsoft.Json.JsonProperty("host_name")>
Public Property host_name As String
Get
End Get
Set(value As String)
End Set
End Property
<Newtonsoft.Json.JsonProperty("u_recovery_time_achievable")>
Public Property u_recovery_time_achievable As String
Get
End Get
Set(value As String)
End Set
End Property
End Class
So if the .Result of the response isn't filled in, my assumption is that I have to run some code to deserialize the data? I tried writing up a line like this here:
Dim lstServers As List(Of ServiceNowServerRecord) = JsonConvert.DeserializeObject(Of List(Of ServiceNowServerRecord))(objServiceNowRESTQueryResponse.RawJSON)
But that throws an exception:
Exception thrown: 'Newtonsoft.Json.JsonSerializationException' in Newtonsoft.Json.dll
Additional information: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[CMDBReconciliation.CMDBReconciliation+ServiceNowServerRecord]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'result', line 1, position 10.
Do you have any ideas for me on how to get my result set back in a format that I can loop through? Like a list of objects? I'm not sure what I am doing wrong...
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I'll play around with it tonight. I've used the compiled C# library in a VB project before so you should be fine there, although I think I've always left my models in C#. Something in the back of my mind seems to recall some quirks with how newtonsoft handles deserialization in VB.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I got it to work via a different method. Not sure if the .Result being blank stuff was VB related or what, but this method seems to work in the interim:
Dim jsonObject = JObject.Parse(objServiceNowRESTQueryResponse.RawJSON)
Dim myDTResults = JsonConvert.DeserializeObject(Of DataTable)(jsonObject("result").ToString())
For Each row As DataRow In myDTResults.Rows
...
Next
But thanks for looking anyway!
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hey there, me again. Trying to use your DLL files in a VB.Net Project. Right now I'm just trying to post an incident ticket. When my POST command runs, it gets an error back, and yet the incident ticket does get created in SN. This is what my post statement looks like:
objServiceNowRecordIncident.caller_id = strCallerID
objServiceNowRecordIncident.assignment_group = strAssigned_Group
objServiceNowRecordIncident.short_description = strSummary
objServiceNowRecordIncident.description = strDetails
objServiceNowRecordIncident.cmdb_ci = strProduct_Name
objServiceNowRecordIncident.impact = strImpact
objServiceNowRecordIncident.urgency = strUrgency
objServiceNowRecordIncident.category = strCategory
objServiceNowRESTSingleResponse = objServiceNowTableAPIClient.Post(objServiceNowRecordIncident)
Every time this post command runs, it does create a ticket, but then I get this exception thrown:
{"Unexpected character encountered while parsing value: {. Path 'result.assignment_group', line 1, position 169."}
I'm grasping at straws here. In case a stack trace would mean anything to you, this is what the exception has for that:
" at Newtonsoft.Json.JsonTextReader.ReadStringValue(ReadType readType)"
" at Newtonsoft.Json.JsonTextReader.ReadAsString()"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)"
" at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)"
" at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)"
" at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)"
" at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)"
" at ServiceNow.TableAPI.TableAPIClient`1.Post(T record)"
" at ServiceNowTicketRequests.ServiceNowTicketRequests.ServiceNow_NewIncidentTicket(String strCallerID, String strSummary, String strDetails, String strService_Type, String strProduct_Name, String strImpact, String strUrgency, String strAssigned_Group, String strAssignee, String strCategory) in \\Pwmonitxxx\code$\ServiceNowTicketRequests\ServiceNowTicketRequests 4.1\ServiceNowTicketRequests.vb:line 1199"
I am not passing anything strange for any of the fields that I am aware of. And every last ticket post throws an exception, no matter how simple I make the incident ticket. So I'm wondering if you may have any insight? Seems like the failure is occurring between the SN response and the POST method within the DLL of yours I'm using. Below is an example of the values I am passing in for my incident fields, so you can see, nothing crazy -- no weird symbols.
strCallerID = "xEventMgmt"
strAssigned_Group = "Systems Monitoring Support"
strSummary = "This is a test of our emergency broadcast system."
strDetails = "Please do not adjust your dials."
strProduct_Name = "Monitoring Utilities"
strImpact = "3"
strUrgency = "3"
strCategory = "Hardware"
Any clues at all for me?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
disregard! I figured it out. When opening an incident ticket, I had it set to return all the fields I had just posted. The problem with that is that, I think, Assignment Group is sort of like nested data, its just a sys_id to a different table. Anyway, I changed it so that my return fields were just number, as that's all I really needed returned back to me, and that worked.
Also, I did figure out my earlier post as well. The Result was not filling in the fields for me, even though I copied your code. Well I'm working in VB and classes and properties work differently between that and C#. I guess in VB.Net you have to create a private variable to actually house the values, whereas C# just does that for you automatically. So basically in my class constructor, I had to add a bunch of lines like below.
Public Class ServiceNowRecordIncident
Inherits Record
Private _cmdb_ci As String
Public Property cmdb_ci As String
Get
Return _cmdb_ci
End Get
Set(value As String)
_cmdb_ci = value
End Set
End Property
So now I've got everything working as needed so far. Thanks for putting these files out there, now that I've got a handle on how to use them, they are VERY helpful!!
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi Ryan,
I have similar issue re: response.
When I make POST call to add record to a table it works perfectly well except I don't get any response back. I tried assigning response to plain string field and I still see nothing in there. Using these in a C# project
Var response = serviceNowClient.UploadString(URL + "?&sysparm_fields=" + fieldList, "POST", data);
logger.LogInformation("response : " + response);
Appreciate your response!
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I'll need to do some more experimentation to confirm but one possibility may be posting a record with related tables (ie Product_Name). While you can dot-traverse a query from ServiceNow in a manner like this:
[JsonProperty("caller_id.first_name")]
public string caller_first_name { get; set; }
I'm not sure this would work for posting a new record to say the Incident table because Caller references another table. I'm thinking for posting a new record we want to stay within the confines of the specific table we are posting to. Of course you need to specify the caller, but I would just use the ResourceLink to reference related properties. I'm not sure if just having the dot traversed properties on your model matters or if just leaving them empty is sufficient.
[JsonProperty("caller_id")]
public ResourceLink caller_id { get; set; }
You may want to query for them ahead of your post so you can use the ID's but posting a ResourceLink with just the value should also work but according to my notes using the value and not the Id can result in ServiceNow failing to add the association silently.
I'll look through some other use cases I have this evening and see what else I can find that might be of help. For now my recommendation would be to try commenting out all related properties from your model and just use the ResourceLink.
I actually haven't used this for creating new Incidents myself. I've used it for Posting projects, related tasks & assignments as well as posting time entries, updating user profiles and retrieving data.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I have looked at a past project I did where I was posting time entries for users from an external system. In this case while I did have a dot-traversed property (user.employee_number), when I was performing the POST operation I was excluding that property and only populating the ResourceLink for user.
I still can't confirm that does the trick specifically since our organization stopped using that third party product so I will have to setup a manual test to confirm.
If it is the related table properties that is causing the issue AND we can avoid the issue by simply not populating them, that might be a good update for the project: To strip out all dot-traversed properties, leaving only the ResourceLink's, before completing the POST operation.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I expanded the 'sys_user' class to bring back a lot more field data. The issue I am running into now is that I want to control which fields are returned back. I don't always want the same fields in a query.
I see that the 'initialize' function in TableAPIClient.cs is responsible for building the '_FieldList' which is used to populate the 'sysparm_fields' in the URL. I don't see a way to override this workflow. Any suggestion?
I have not looked into the create or update features, and have a feeling that these will also use all the fields in the class opposed to the one that I want to use.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
In the RESTQUeryResponse function, we are building and passing the URL for the data to be pulled back:
response.RawJSON = ServiceNowClient.DownloadString(URL + "?&sysparm_fields=" + _FieldList + "&sysparm_query=" + query);
The amount of records coming back is being capped at 10,000 records, but I am needing the other remaining records.
SNow documentation states that the following two variables are used to bring the data back in chunks:
- sysparm_limit
- sysparm_offset
Looking at the current response data, I am not seeing data/links being generated that could be used to tell how many records are actually in the table or a url to pull the next chunk. There currently is no way to specify the offset or limit to bring back the next chunk of data. Any pointers or idea how to implement something like this.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thought I would provide an update on my problem. It was a quick hack, but it works. What I did was to add another overloaded initialize(....) function that took a string value, which contains the comma separated list of fields I want back. I also created another overloaded TableAPIClient(...) function that took a string value for the field list.
In my main application, I create the list of fields I want back (has to be from the list of fields in the sys_user Class in order to leverage the decoding results workflow). I preformatted the list to match what is generated normally:
string bgFieldList = "sys_id,user_name,first_name,last_name,name,email,company.name,manager";
TableAPIClient<sys_user> userClient = new TableAPIClient<sys_user>("sys_user", cbo_My_Instance.Text, txt_My_UserID.Text, txt_My_Password.Text, bgFieldList );
Then in the ServiceNow project, I add the following two functions. The TableAPIClient function is called on the creation of my variable, which in turn calls the initialize function. My string of field names is passed through the functions to where they get used:
public TableAPIClient(String tableName, String instanceName, String userName, string password, String bg_FieldList)
{
NetworkCredential credentials = new NetworkCredential { UserName = userName, Password = password };
initialize(tableName, instanceName, credentials, bg_FieldList);
}
private void initialize(String tableName, String instanceName, NetworkCredential credentials, String bg_FieldList)
{
_TableName = tableName;
_InstanceName = instanceName;
// Initialize the Web Client
ServiceNowClient = new WebClient();
ServiceNowClient.Credentials = credentials;
// Build the field list from the record type that will be retrieved
_FieldList = "";
_FieldList = bg_FieldList;
}
It is a bit of a hack, but it works for what I was needing. My Query and Create processes use different combination of fields, so creating a second version of the sys_user class with the limited fields I want would solve some of the problems. Query using the sys_user class that has the large number of fields, and Create using another sys_user2 version of the class with just the fields I need to do the initial create.
A method to add/remove fields to/from a class on the fly would be a nice feature.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I ran into an issue where my ServiceNow connection was needing OAuth2 for connecting to our ServiceNow instance, and Proxy raised its head as well. The comments below show how I added the ability to use a Proxy in all my API calls (while writing this I found a way to simplify the code more which is a bonus. Less code...). Hopefully this can help others that may be having issues.
I did not include any of the OAuth2 code as I have not gotten that far. Tokens have a 3-30 minute life expectancy, so I have to figure out where to add the OAuth code. Have it working in Postman though.
I created 2 new classes under the TableAPI project, which hold ServiceNow credentials, and Proxy/OAuth data. The TableAPIClient.cs file we only need to create a global variable and update the TableAPIClient and initialize functions. I chose to create new version of the functions, which keeps things as they are currently, but allows me to override to use my own code.
Under the TableAPI project, I created a new class called OAuthContext.cs. In this file we set the Proxy, OAuth, and any other information to be used. The following is a stripped down version of the class, showing just what you need for setting up the Proxy.
public class OAuthContext
{
public bool EnableProxy { get; set; } / Proxy flag
public bool EnableOAuth { get; set; } // OAuth2 flag
// Used for Proxy if needed
public string Proxy_Url { get; set; }
public string Proxy_User { get; set; }
public string Proxy_Pwd { get; set; }
public Int32 Proxy_Port { get; set; }
...
}
In the TableAPIClient.cs file, we add a global variable line, which is for the class we created above:
private OAuthContext _OAuthContext;
We then add a new constructor to handle the object:
public TableAPIClient(String tableName, UserContext b_Context, OAuthContext b_oAuthContext)
{
// Holds various connection details - New for 07/19/2025
_OAuthContext = b_oAuthContext;
NetworkCredential credentials = new NetworkCredential { UserName = b_Context.UserID, Password = b_Context.Password };
initialize(tableName, b_Context.Instance, credentials);
}
In all the initialize functions, you just need to add the following block of code just after the "ServiceNowClient.Credentials = credentials;" line:
// Setup for Proxy
if (_OAuthContext.EnableProxy == true)
{
WebProxy proxy = new WebProxy(_OAuthContext.Proxy_Url, _OAuthContext.Proxy_Port);
proxy.Credentials = new NetworkCredential(_OAuthContext.Proxy_User, _OAuthContext.Proxy_Pwd);
proxy.UseDefaultCredentials = false;
proxy.BypassProxyOnLocal = false; //still use the proxy for local addresses
ServiceNowClient.Proxy = proxy;
}
Basically what your doing here is looking into the global _OAuthContext object variable we setup at the beginning, and if the Proxy flag was set, we would then run the code in this If block. We add the Proxy information into the ServiceNowClient object, which has the API call use the proxy information supplied.
In my main application, I reference the library DLL created with the above changes. I create a global variable in my project to hold the Proxy information. You could also just use the project resources to hold this information.
OAuthContext BEG_AuthContext; // Used for the Auth and Proxy
When my form loads, I populate the global variable with the necessary data. Note that I also have a "UserContext.cs" class which is used to hold my ServiceNow connection information. This way I only need to pass one object in my function calls opposed to a number of variables:
BEG_Context = new UserContext();
BEG_AuthContext = new OAuthContext();
BEG_AuthContext.EnableOAuth = false;
BEG_AuthContext.EnableProxy = true;
BEG_Context.UserID = txt_My_UserID.Text;
BEG_Context.Password = txt_My_Password.Text;
BEG_Context.Instance = cbo_My_Instance.Text;
BEG_AuthContext.Proxy_Pwd = txt_ProxyPassword.Text;
BEG_AuthContext.Proxy_Url = txt_ProxyURL.Text;
BEG_AuthContext.Proxy_User = txt_ProxyUserID.Text;
BEG_AuthContext.Proxy_Port = Convert.ToInt32(txt_ProxyPort.Text);
So now when I need to pull back data, I do so like this:
private void PullData(string b_Query)
{
// Pass in the Table Name, SNow credentials, and Proxy-OAuth credentials
TableAPIClient<core_company> companyClient = new TableAPIClient<core_company>("core_company", BEG_Context, BEG_AuthContext);
IRestQueryResponse<core_company> response = companyClient.GetByQuery(b_Query);
}
So when we create the companyClient variable, we pass in the table were working with, the SNow credentials to use, as well as the Proxy-OAuth2 details.
Tracing through the code, you would first hit the TableAPIClient function, followed by the initialize function. Then finally we would call the RESTQueryResponse<T> GetByQuery function which uses all the stuff that we setup.
I left out all the OAuth2 code as that is something that I am still working on. The proxy stuff is working correctly so far with out issues.
The UserContext.cs class I created under the TableAPI project looks similar to the following. It is easier to just pass one object than a number of variables:
public class UserContext
{
public string Password { get; set; }
public string UserID { get; set; }
public string Instance { get; set; }
. . .
}
So far this API has worked really well for me. As I work more with it and expand it, I am starting to understand more of the C# concepts and how this library works.