CHAPTER 2
Incident management, which can be defined as a series of steps taken to resolve a customer’s problem, lies at the heart of customer service. And clear communication is the key to success when managing customer incidents.
Managing expectations—both the customer’s and your team’s—is the essential element of good communication. In short, your customers should know what to expect after they report a problem (a clear roadmap with an ongoing feedback loop is essential), and your manager should know how much time and effort you will spend on a particular incident.
Managing the expectations of a customer’s incident begins with asking if what the user and your boss expect from you is achievable. Before answering, you must also ask yourself what you can reasonably expect from yourself as far as time constraints go.
If what is being asked is unreasonable, you must take a step back and make this clear to all parties (primarily your boss) and get back to the customer with a coordinated explanation of why responding to the current request is not feasible. If, on the other hand, the request is reasonable, you should respond as quickly as possible with a clear time estimation and an indication of the steps involved in solving the customer’s problem.
Managing expectations for a customer’s incident means understanding the following:
Incident management also involves determining how much communication is required, the tone of communication, and, most importantly, the frequency of communication.
In this chapter, we will automate incident management by creating a C# solution that includes expectation management and the communication loop required to successfully close any reported incident. Most helpdesk and CRM tools do not focus on expectation management or the tone and frequency of communication, but these tools should be useful when included in your projects or products directly, and they will allow you to integrate these capabilities out of the box in your company’s line of products.
CRM is the approach taken in order to manage a company’s interaction with current and future customers.
Most current CRM software, such as Salesforce and Microsoft Dynamics, focuses more on closing a sales cycle than on incident management itself. Although most CRM software includes a helpdesk subset, none of the CRM tools I've examined (even those built specifically for customer support, e.g., Zendesk) give much importance to expectation management and frequency or tone of communication.
My suggestion is that you offer a C# class responsible for managing expectations around communication, and the frequency and tone of communication related to any reported customer incident. This class can serve as an embeddable, small incident-management-oriented CRM system within your projects or products, allowing customers to report a problem to your CORD helpdesk within the product itself.
Table 4 represents the specs that our Simple Awesome CRM Tool will emphasize.
Table 4: Specs for the Simple Awesome CRM Tool
Allow the user within the product to report an issue to the CORD helpdesk. |
Clearly define and redefine the customer’s expectations of the issue. |
Manage and set the frequency of the communication. |
Report back to the user the state and stage of the incident until resolution. |
Indicate to the helpdesk worker the need to follow up or provide an update. |
Indicate to the user the resource allocated for the task and its availability. |
With these thoughts in writing, let’s consider how we might code this. Ideally, the Simple Awesome CRM Tool should be able to store its data in the cloud, so that it can be easily stored and retrieved from any location.
When it comes to storing data in the cloud, there are myriad options available using both traditional relational databases (e.g., MySQL, SQL Server, Postgres SQL, Oracle, etc.) and NoSQL (e.g., MongoDB, DynamoDB, RavenDB, etc.) that can be hosted privately or, alternatively, hosted on services like Amazon Web Services (AWS) or Microsoft Azure.
Although setting up any of those relational or nonrelational (NoSQL) databases on AWS or Azure has been greatly simplified in recent years, you must still provision instances and throughput, and you must consider factors such as requests and pricing in order to properly deploy an online data store. That’s quite a process and, frankly, a very demanding one.
Our objective is to build a simple CRM tool that can be easily embedded into existing projects and products without causing us to break our heads on deploying an online data store.
Table 5 represents what our data store should allow our Simple Awesome CRM Tool to achieve.
Table 5: Simple Awesome CRM Tool Data Store Specs
Zero maintenance and run out of the box. |
Scalable without restrictions on throughput and requests. |
Inexpensive, schema-less, excellent security features. |
NoSQL (nonrelational) DB but with SQL language capabilities. |
Now, wait a second. At first sight it seems we are asking a bit too much. An almost infinitely scalable, schema-less NoSQL DB that allows SQL-compatible querying capabilities? Does this actually exist?
Well, yes, such a marvel does indeed exist. The folks at Microsoft have developed an amazingly flexible and impressive, fully hosted NoSQL document (JSON) database that has SQL querying language capabilities. It is called DocumentDB and can be found at Microsoft Azure.
In order to write the Simple Awesome CRM Tool, we’ll use the .NET SDK for DocumentDB, which can be found on GitHub.
Although the code examples shown are quite intuitive for following along, I recommend that you have a look at the excellent DocumentDB online documentation.
According to the specs in Table 4, we will need to create a base C# class that should be able to:
We’ll need to create a DocumentDB instance on Microsoft Azure, and you’ll need to sign in or sign up for Azure with a Microsoft account. You can do that by visiting azure.microsoft.com.

Figure 1: Microsoft Azure Sign-In Screen
Once you’ve signed up for or signed in to the Azure portal, you can browse through the list of Azure services and select the DocumentDB Accounts option.

Figure 2: DocumentDB within the List of Azure Services
After you select DocumentDB, you must create a new DocumentDB account by clicking Add.

Figure 3: Screen to Add a DocumentDB Account
This will lead to a screen that allows you to enter the details of the DocumentDB account: ID, NoSQL API, Subscription, Resource Group, and Location. You can select the Azure region that will host your account.
The ID is a unique global identifier within Microsoft Azure for the DocumentDB account. To finalize the creation of the account, click Create. The DocumentDB account creation process can take a few minutes.

Figure 4: Final DocumentDB Account Creation Screen
Figure 5 depicts how the DocumentDB account will appear after it is created.

Figure 5: DocumentDB Account Dashboard
A DocumentDB account can host multiple DocumentDB databases, but simply having a DocumentDB account doesn’t mean you’ll have a DocumentDB database ready for use. However, such a database can be created by clicking Add Database.
The only requirement is that you provide an ID for the new DocumentDB database.

Figure 6: New DocumentDB Database Creation Screen
When the DocumentDB has been created, it will be configurable through an intuitive dashboard.

Figure 7: DocumentDB Database Dashboard
In DocumentDB, JSON documents are stored under collections. A collection is a container of JSON documents and the associated JavaScript application logic (user functions, stored procedures, and triggers).
Figure 8 depicts how DocumentDB is structured.

Figure 8: DocumentDB Internal Structure
A collection is a billable entity in which the cost is determined by the performance level associated with the collection. The performance levels (S1, S2, and S3) provide 10GB of storage and a fixed amount of throughput.

Figure 9: DocumentDB Collection’s Performance Levels and Pricing Tier
A Request Unit (RU) is measurable in seconds. For example, on the S1 level, 250 RUs (API calls) can be executed per second. More information about performance levels in DocumentDB can be found here.
When creating a collection, the default pricing tier is S2. However, for demos and Proof of Concepts (POCs), an S1 tier is enough.
A collection can be created by clicking Add Collection on the dashboard. You will need to specify the pricing tier (by selecting S1) and the indexing policy. The indexing policy’s default setting is in fact Default. If you want to take advantage of full string queries, simply change the indexing policy from Default to Hash in the Scale & Settings options after creating the collection. Open the Indexing Policy options and change the indexingMode property from consistent to hash. For the Simple Awesome CRM, we will be using the Hash indexing policy.
|
|
Figure 10: Creating a DocumentDB Collection and Changing the Indexing Policy
Now that the DocumentDB account, database, and collection are ready on Microsoft Azure, you must next install the .NET DocumentDB Client library from NuGet called Microsoft.Azure.DocumentDB in order to start coding.
First, launch Visual Studio 2015 and create a C# Console Application. After the template code has loaded, go to Tools > NuGet Package Manager > Manage NuGet Packages and select nuget.org in the Package Source drop-down control. In the search box, type DocumentDB and the package will be displayed and available for installation.

Figure 11: Installing the .NET DocumentDB Client as a NuGet Package with Visual Studio 2015
Once the .NET DocumentDB Client has been installed, you will have the Microsoft.Azure.Documents.Client and Newtonsoft.Json assemblies referenced in your Visual Studio solution.
With DocumentDB wired up and ready, we can start coding.
Based on the guidelines set forth, our Simple Awesome CRM software should be able to allow a user to open incidents, find incidents by attributes, change incident attributes, add comments, and allow the helpdesk worker to indicate any resources allocated for addressing an incident.
DocumentDB has support for range-based indexes on numeric fields that allow you to do range queries (e.g., where field > 10 and field < 20). In order to avoid doing costly scans when making range queries on dates (e.g., records older than yesterday or records placed last week), we need to convert the string representation of a date to a number. This will allow us to use range indexes on these fields.
We will be treating DateTime values as epoch values (the number of seconds since a particular date). In this class we are using 1 January 1970 00:00 as a starting point; however, you are free to use a different value.
Let’s now examine how we can implement this with Visual Studio 2015 and .NET 4.5.2.
First, we will create a namespace called CrmCore.DateEpoch that will be responsible for implementing epoch values. This will include the DateEpoch and Extension classes.
Later, we will need to implement a CrmCore.Enum namespace where the enum definitions will be defined, and we’ll need to create a utility namespace called CrmCore.EnumUtils.
Later we will also need to implement a CrmCore.Enum namespace where the enum definitions will be defined, and we’ll need a utility namespace called CrmCore.EnumUtils.
Code Listing 1: Implementing Epoch
using System; namespace CrmCore.DateEpoch { public static class Extensions { public static int ToEpoch(this DateTime date) { if (date == null) return int.MinValue; DateTime epoch = new DateTime(1970, 1, 1); TimeSpan epochTimeSpan = date - epoch; return (int)epochTimeSpan.TotalSeconds; } } public class DateEpoch { public DateTime Date { get; set; } public int Epoch { get { return (Date.Equals(null) || Date.Equals(DateTime.MinValue)) ? DateTime.UtcNow.ToEpoch() : Date.ToEpoch(); } } public DateEpoch(DateTime dt) { Date = dt; } } } |
Now that we have seen how dates are going to be handled by DocumentDB, we also need to define several Enum types that will be used to indicate the status, severity, feedback frequency, and type of communication of an incident.
Code Listing 2: Enums Representing States of an Incident
using System; namespace CrmCore.Enums { public enum IncidentSeverity { [Description("Urgent")] Urgent, [Description("High")] High, [Description("Normal")] Normal, [Description("Low")] Low }; public enum IncidentFeedbackFrequency { [Description("Hourly")] Hourly, [Description("Every4Hours")] Every4Hours, [Description("Every8Hours")] Every8Hours, [Description("Daily")] Daily, [Description("Every2Days")] Every2Days, [Description("Weekly")] Weekly, [Description("Every2Weeks")] Every2Weeks, [Description("Monthly")] Monthly }; public enum IncidentCommunicationType { [Description("ReceiveUpdatesOnly")] ReceiveUpdatesOnly, [Description("Bidirectional")] Bidirectional }; public enum IncidentStatus { [Description("NotReported")] NotReported, [Description("Reported")] Reported, [Description("FeedbackRequested")] FeedbackRequested, [Description("UnderAnalysis")] UnderAnalysis, [Description("IssueFound")] IssueFound, [Description("WorkingOnFix")] WorkingOnFix, [Description("FixDelivered")] FixDelivered, [Description("FixAccepted")] FixAccepted, [Description("Solved")] Solved, [Description("Closed")] Closed, [Description("Reopen")] Reopen }; } |
With the IncidentSeverity, IncidentFeedbackFrequency, IncidentCommunicationType, and IncidentStatus in place, we have the necessary categories to assign to any incident submitted using the system.
However, given that DocumentDB works with JSON, it is important to understand that a C# enum will be stored as an integer when converted to JSON, which can make it difficult to read when browsing through the list of documents on DocumentDB.
Therefore, in order for any Enum value to be readable as a string, we must use System.ComponentModel and System.Reflection before storing the value on DocumentDB. Let’s implement an EnumUtils class to take care of this process.
Code Listing 3: EnumUtils Class
using System; using System.ComponentModel; using System.Reflection; namespace CrmCore.EnumUtils { public class EnumUtils { protected const string cStrExcep = "The string is not a description or value of the enum."; public static string stringValueOf(Enum value) { FieldInfo fi = value.GetType().GetField(value.ToString()); DescriptionAttribute[] attributes = (DescriptionAttribute[]) fi.GetCustomAttributes(typeof(DescriptionAttribute), false); return (attributes.Length > 0) ? attributes[0].Description : value.ToString(); } public static object enumValueOf(string value, Type enumType) { string[] names = Enum.GetNames(enumType); foreach (string name in names) { if (stringValueOf((Enum)Enum.Parse(enumType, name)).Equals(value)) { return Enum.Parse(enumType, name); } } throw new ArgumentException(cStrExcep); } } } |
The stringValueOf method converts an integer Enum value to its string representation, and enumValueOf takes a string value and converts it to its corresponding integer Enum value (given its type).
EnumUtils is an incredibly handy and essential part of the Simple Awesome CRM system, as it will be used across many of the methods that follow.
Now that we know how to deal with dates in DocumentDB and categorize incidents by using Enum types, let’s create the classes that will store incident details.
Code Listing 4: Incident Details Classes
using CrmCore.EnumUtils; using CrmCore.Enums; using CrmCore.DateEpoch; using System; using System.ComponentModel; using System.Reflection; namespace CrmCore.IncidentInfo { public sealed class AllocatedResource { private IncidentStatus stage; public string Engineer { get; set; } public string Stage { get { return EnumUtils.stringValueOf(stage); } set { stage = (IncidentStatus)EnumUtils. enumValueOf(value, typeof(IncidentStatus)); } } public DateEpoch Start { get; set; } public DateEpoch End { get; set; } } public sealed class Comment { public string Description { get; set; } public string UserId { get; set; } public string AttachmentUrl { get; set; } public DateEpoch When { get; set; } } public sealed class IncidentInfo { private IncidentSeverity severity; private IncidentStatus status; private IncidentFeedbackFrequency feedbackFrequency; private IncidentCommunicationType communicationType; public string Description { get; set; } public string Severity { get { return EnumUtils.stringValueOf(severity); } set { severity = (IncidentSeverity)EnumUtils. enumValueOf(value, typeof(IncidentSeverity)); } } public string Status { get { return EnumUtils.stringValueOf(status); } set { status = (IncidentStatus)EnumUtils. enumValueOf(value, typeof(IncidentStatus)); } } public string FeedbackFrequency { get { return EnumUtils.stringValueOf(feedbackFrequency); } set { feedbackFrequency = (IncidentFeedbackFrequency)EnumUtils. enumValueOf(value,typeof(IncidentFeedbackFrequency)); } } public string CommunicationType { get { return EnumUtils.stringValueOf(communicationType); } set { communicationType = (IncidentCommunicationType)EnumUtils. enumValueOf(value,typeof(IncidentCommunicationType)); } } public AllocatedResource[] Resources { get; set; } public Comment[] Comments { get; set; } public DateEpoch Opened { get; set; } public DateEpoch Closed { get; set; } } } |
Each specific incident’s details are stored as a single JSON document within DocumentDB, which, in C#, is represented by the IncidentInfo class. The IncidentInfo class is sealed, which indicates that it is a simply a container for data. It might contain no comments or resources (as object arrays), or it might contain multiple items.
A Comment object represents an instance of a comment that is related to a particular incident. An AllocatedResource object represents an instance of a resource that is assigned to a particular incident.
This is all good, but we are not yet able to do anything meaningful with IncidentInfo or its related classes, because so far they are mere data definitions.
In order to interact with DocumentDB and use IncidentInfo, we must create an Incident class that will serve as the class responsible for posting and retrieving any incident data (IncidentInfo) to DocumentDB.
Code Listing 5 includes the complete code for the Incident class. Next, we will examine each method individually.
Code Listing 5: Incident Class Implementation
In order to understand how our Simple Awesome CRM works with DocumentDB, let’s take a detailed look at each method within the Incident class, and let’s make sure we understand what each of the following items do.
Code Listing 6: Important Initialization Details for DocumentDB
private const string docDbUrl = "dbs/SimpleAwesomeCrm/colls/CrmObjects"; private const string docDbEndpointUrl = "https://fastapps.documents.azure.com:443/"; private const string docDbAuthorizationKey = "<<DocDb Key>>"; protected DocumentClient client = null; |
The docDbUrl string represents the relative URL to the CrmObjects collection that was created within DocumentDB using the Azure portal. The URL is formed as follows:
dbs/<DocumentDB database name>/colls/<DocumentDB collection name>
In our case, SimpleAwesomeCrm is the DocumentDB database name, and CrmObjects is the DocumentDB collection name (which is where our CRM JSON documents will be stored).
The docDbEndpointUrl string represents the actual URL of the DocumentDB instance running on Azure. We’ve chosen the subdomain “fastapps,” but you may use anything else (so long as it corresponds to what you defined when creating the DocumentDB account on the Azure Portal).
The docDbAuthorizationKey string is the Azure access key to the DocumentDB instance specified by docDbEndpointUrl.
The client object is the DocumentDB Azure SDK for .NET instance class that is responsible for all the communication with DocumentDB.
Now we can establish a connection to DocumentDB.
Code Listing 7: Connection to DocumentDB
private void Connect2DocDb() { client = new DocumentClient(new Uri(docDbEndpointUrl), docDbAuthorizationKey); } |
The connection to DocumentDB is made by creating an instance of DocumentClient that will be properly disposed later.
Code Listing 8: Disposal of the DocumentClient Instance
protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) client.Dispose(); } disposed = true; } |
With a connection established, we can now open an incident and submit it to DocumentDB. We can do this as shown in Code Listing 9.
Code Listing 9: Opening an Incident
public async Task<Document> Open(string description, bool check = false) { if (client == null) Connect2DocDb(); if (info != null && client != null) { info.Description = description; Document id = await client.CreateDocumentAsync(docDbUrl, info); if (check) return (id != null) ? client.CreateDocumentQuery(docDbUrl). Where(d => d.Id == id.Id).AsEnumerable().FirstOrDefault() : null; else return id; } else return null; } |
The Open method makes a connection to DocumentDB when client (the DocumentClient instance) is null—i.e. when a connection has not yet been established.
Once the connection is established, CreateDocumentAsync is called with docDbUrl and an instance of IncidentInfo. To check if the document was successfully submitted to DocumentDB, the check parameter can be set to true.
However, in order for this to work, an instance of IncidentInfo needs to be created. Code Listing 10 shows how we can do that from a wrapper.
Code Listing 10: Wrapper around Incident.Open
OpenFindCase wraps a call around CrmExample.OpenCase, which creates an Incident instance and calls the Open method of that instance. This returns a Document instance that is then deserialized into an IncidentInfo object. Finally, this IncidentInfo object is printed on the screen by OutputCaseDetails. Running this example will produce the result shown in Figure 12.

Figure 12: Incident Creation Console Output
The document can be seen on Azure, as in Figure 13.

Figure 13: Incident in the Azure Document Explorer
When a document is created with no particular specifications, DocumentDB automatically assigns a random GUID to it—in this case: ce333e7b-9d6a-49e2-a6b9-d0b89d3c5086.
Now that we have the ability to create incidents, we will shift our focus to retrieving and finding documents in Azure. Given that we should be able to search on any specific criteria using any of the fields of an IncidentInfo object, we next need to add several Find methods to the Incident class.
Code Listing 11: Find Methods of the Incident Class
public IEnumerable<Document> FindById(string id) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery(docDbUrl) where c.Id == id select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByDescription(string description) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Description.ToUpper(). Contains(description.ToUpper()) select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByDateOpenedAfter(DateTime opened) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Opened.Epoch >= opened.ToEpoch() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByDateOpenedBefore(DateTime opened) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Opened.Epoch < opened.ToEpoch() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByDateOpenedBetween(DateTime start, DateTime end) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Opened.Epoch >= start.ToEpoch() where c.Opened.Epoch < end.ToEpoch() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByStatus(IncidentStatus status) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Status == status.ToString() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindBySeverity(IncidentSeverity severity) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.Severity == severity.ToString() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByFeedbackFrequency (IncidentFeedbackFrequency ff) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.FeedbackFrequency == ff.ToString() select c; return cases; } else return null; } public IEnumerable<IncidentInfo> FindByCommunicationType (IncidentCommunicationType ct) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery<IncidentInfo>(docDbUrl) where c.CommunicationType == ct.ToString() select c; return cases; } else return null; } |
All the Find methods of the Incident class are similar—they each start by attempting to connect to DocumentDB using Connect2DocDb and, if a connection exists (when client is not null), a LINQ query is executed with a specific condition. In most cases an IEnumerable<IncidentInfo> instance is returned with the results from the LINQ query.
IEnumerable is preferable because it describes behavior, while List is an implementation of that behavior. When you use IEnumerable, you give the compiler a chance to defer work until later, allowing for possible optimization along the way. If you use List, you force the compiler to check the results immediately.
Whenever you use LINQ expressions, you should also use IEnumerable, because only when you specify the behavior will you give LINQ a chance to defer evaluation and optimize the program.
Given that there could be one or more results, the returned IEnumerable<IncidentInfo> object needs to be parsed, and each result should be printed out. In order to do this, we will use our wrapper CrmExample class.
Let’s create our first wrapper for the method FindByDescription within the CrmExample class.
Code Listing 12: FindByDescription Wrapper
public static void FindByDescription(string description) { using (Incident inc = new Incident()) { IEnumerable<IncidentInfo> issues = inc.FindByDescription(description); Console.WriteLine("FindByDescription: " + description); if (issues.Count() > 0) { Console.Write(Environment.NewLine); foreach (var issue in issues) { OutputCaseDetails(issue); Console.Write(Environment.NewLine); } } Console.WriteLine(cStrTotalResults + issues.Count().ToString()); } } |
Code Listing 13 depicts how calling it from the main program would follow.
Code Listing 13: FindByDescription Invocation
CrmExample.FindByDescription("Exception"); |
Figures 14 and 15 show the FindByDescription results on screen and in Azure.

Figure 14: FindByDescription Results on Screen

Figure 15: FindByDescription Results in Azure
Let’s now examine the method FindByDateOpenedAfter. This will retrieve any documents from DocumentDB that were opened with a DateTime equal to or greater than the parameter being used for searching.
Code Listing 14: FindByDateOpenedAfter Wrapper
public static void FindByDateOpenedAfter(DateTime opened) { using (Incident inc = new Incident()) { IEnumerable<IncidentInfo> issues = inc.FindByDateOpenedAfter(opened); Console.WriteLine("FindByDateOpenedAfter: " + opened.ToString(cStrDateTimeFormat)); if (issues.Count() > 0) { Console.Write(Environment.NewLine); foreach (var issue in issues) { OutputCaseDetails(issue); Console.Write(Environment.NewLine); } } Console.WriteLine(cStrTotalResults + issues.Count().ToString()); } } |
Code Listing 15 depicts calling FindByDateOpenedAfter from the main program.
Code Listing 15: FindByDateOpenedAfter Invocation
CrmExample.FindByDateOpenedAfter(new DateTime(2016, 3, 4, 16, 03, 58, DateTimeKind.Utc)); |
That action produces the results in Figures 16 and 17.

Figure 16: FindByDateOpenedAfter Results on Screen

Figure 17: FindByDateOpenedAfter Results in Azure
Next, let’s examine the method FindBySeverity. This will retrieve any documents from DocumentDB that have a specific IncidentSeverity assigned.
Code Listing 16: FindBySeverity Wrapper
public static void FindBySeverity(IncidentSeverity severity) { using (Incident inc = new Incident()) { IEnumerable<IncidentInfo> issues = inc.FindBySeverity(severity); Console.WriteLine("FindBySeverity: " + EnumUtils.stringValueOf(severity)); if (issues.Count() > 0) { Console.Write(Environment.NewLine); foreach (var issue in issues) { OutputCaseDetails(issue); Console.Write(Environment.NewLine); } } Console.WriteLine(cStrTotalResults + issues.Count().ToString()); } } |
Code Listing 17 depicts calling FindBySeverity from the main program.
Code Listing 17: FindBySeverity Invocation
CrmExample.FindBySeverity(IncidentSeverity.Urgent); |
Figure 18 shows the results.

Figure 18: FindBySeverity Results on Screen
We’ve seen how easy it is to retrieve documents from DocumentDB using some of the Find methods of the Incident class wrapped around CrmExample. Note that the key differentiator among the Find methods is how they differ internally in each of their LINQ queries.
At this stage we know how to open incidents and find them using different LINQ search queries throughout the Find methods implemented. IncidentInfo was designed so that any incident’s allocated comments and resources can vary from none to multiple items.
Both comments and resources are arrays of the Comments and AllocatedResource classes. Both are null when a new incident is opened, so to add comments and resources, we must define a method in order to add each.
A Comment instance will include a Description, UserId (person submitting the comment), AttachmentUrl (URL location of an attachment related to the comment), and When (DateTime it was submitted).
Let’s explore how to implement a method within the Incident class in order to add a comment to an incident.
Code Listing 18: AddComment Implementation
public async Task<IncidentInfo> AddComment(string id, string userId, string comment, string attachUrl) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery(docDbUrl) where c.Id.ToUpper().Contains(id.ToUpper()) select c; IncidentInfo issue = null; Document oDoc = null; foreach (var cs in cases) { var it = await client.ReadDocumentAsync(cs.AltLink); oDoc = it; issue = (IncidentInfo)(dynamic)it.Resource; break; } if (oDoc != null) { Comment c = new Comment(); c.AttachmentUrl = attachUrl; c.Description = comment; c.UserId = userId; c.When = new DateEpoch(DateTime.UtcNow); List<Comment> cMts = new List<Comment>(); if (issue?.Comments?.Length > 0) { cMts.AddRange(issue.Comments); cMts.Add(c); oDoc.SetPropertyValue(cStrComments, cMts.ToArray()); } else { cMts.Add(c); oDoc.SetPropertyValue(cStrComments, cMts.ToArray()); } var updated = await client.ReplaceDocumentAsync(oDoc); issue = (IncidentInfo)(dynamic)updated.Resource; } return issue; } else return null; } |
AddComment first checks whether or not there is a connection to DocumentDB, and if there isn’t, it establishes a connection by invoking Connect2DocDb.
With the connection to DocumentDB in place, performing a LINQ query using CreateDocumentQuery comes next. This is done by using the ID of the incident to which we want to add the comment.
The method CreateDocumentQuery returns an IOrderedQueryable<Document> instance that we will need to loop through (even though the count on IOrderedQueryable<Document> might be 1) in order to perform a ReadDocumentAsync and retrieve the specific Document instance corresponding to the ID being queried.
The ReadDocumentAsync method expects the AltLink (alternative URL link) of the document that will be read from DocumentDB. AltLink is a property of the Document instance that represents the URL of the document within the DocumentDB collection we want to get.
Here’s the interesting bit: to get the IncidentInfo instance from the returned result of the call to ReadDocumentAsync, we must cast the result as issue = (IncidentInfo)(dynamic)it.Resource; where this will be converted into an IncidentInfo instance.
Following that, an array instance of Comment is created and added to the existing Document instance that represents the DocumentDB document (incident) being queried. The array instance of Comment is added through the instance of the Document object by using the property SetPropertyValue.
Once the Comment object has been added to the Document instance, ReplaceDocumentAsync is called and the document with the added Comment is written back to the DocumentDB collection (updated in Azure). The updated IncidentInfo object representing that updated document is returned to the caller of AddComment.
To implement AddComment, we’ll need to create a wrapper around it within CrmExample. Let’s see how this can be done.
Code Listing 19: AddComment Wrapper
public static async void AddComment(string id, string userId, string comment, string attachUrl) { using (Incident inc = new Incident()) { await Task.Run( async () => { IncidentInfo ic = await inc.AddComment(id, userId, comment, attachUrl); OutputCaseDetails(ic); }); } } |
Code Listing 20 depicts how it will be called from the main program.
Code Listing 20: AddComment Invocation
CrmExample.AddComment("4b992d62-4750-47d2-ac4a-dbce2ce85c12", "efreitas", "Request for feedback", "https://eu3.salesforce.com/..."); |
The results are shown in Figure 19.

Figure 19: AddComment Results in Azure
In order to add resources to an incident, we will need to take the same approach as the AddComment method. This means we’ll implement an AddResource method within the Incident class and also create a wrapper around it in CrmExample.
Code Listing 21: AddResource Implementation
public async Task<IncidentInfo> AddResource(string id, IncidentStatus stage, string engineer, DateTime st, DateTime end) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery(docDbUrl) where c.Id.ToUpper().Contains(id.ToUpper()) select c; IncidentInfo issue = null; Document oDoc = null; foreach (var cs in cases) { var it = await client.ReadDocumentAsync(cs.AltLink); oDoc = it; issue = (IncidentInfo)(dynamic)it.Resource; break; } if (oDoc != null) { AllocatedResource rc = new AllocatedResource(); rc.End = new DateEpoch((end != null) ? end.ToUniversalTime() : DateTime.UtcNow); rc.Engineer = engineer; rc.Stage = EnumUtils.stringValueOf(stage); rc.Start = new DateEpoch((st != null) ? st.ToUniversalTime() : DateTime.UtcNow); List<AllocatedResource> rRsc = new List<AllocatedResource>(); if (issue?.Resources?.Length > 0) { rRsc.AddRange(issue.Resources); rRsc.Add(rc); oDoc.SetPropertyValue(cStrResources, rRsc.ToArray()); } else { rRsc.Add(rc); oDoc.SetPropertyValue(cStrResources, rRsc.ToArray()); } var updated = await client.ReplaceDocumentAsync(oDoc); issue = (IncidentInfo)(dynamic)updated.Resource; } return issue; } else return null; } |
As you can see, the implementation of AddResource is almost identical to the one from AddComment.
AddResource first checks whether or not there is a connection to DocumentDB, and if there isn’t, it establishes a connection by invoking Connect2DocDb.
With the connection to DocumentDB in place, performing a LINQ query using CreateDocumentQuery comes next. This is done by using the ID of the incident to which we want to add the resource.
The method CreateDocumentQuery returns an IOrderedQueryable<Document> collection that we will need to loop through in order to perform a ReadDocumentAsync and retrieve the specific Document instance corresponding to the ID of the document being queried.
The ReadDocumentAsync method expects the AltLink to be provided.
To get the IncidentInfo instance from the returned result of the call to ReadDocumentAsync, we must cast the result as issue = (IncidentInfo)(dynamic)it.Resource; where this will be converted into an IncidentInfo instance.
An array instance of AllocatedResource is created and added to the existing Document instance that represents the DocumentDB document (incident) being queried. The array instance of AllocatedResource is added through the instance of the Document by using the SetPropertyValue method.
Once the AllocatedResource property has been added to the Document instance, ReplaceDocumentAsync is called and the document with the added AllocatedResource is written back to the DocumentDB collection. The updated IncidentInfo object representing that updated document is returned to the caller of AddResource.
For this to work, AddResource needs to be wrapped and exposed from the CrmExample class.
Code Listing 22: AddResource Wrapper
Finally, as Code Listing 23 shows, AddResource can be called from the main program.
Code Listing 23: AddResource Invocation
CrmExample.AddResource("4b992d62-4750-47d2-ac4a-dbce2ce85c12", IncidentStatus.IssueFound, "bob smith", DateTime.Now, DateTime.Now); |
The output of this can also be seen in Figure 19.
Although it might be useful at some point to know how to delete comments and allocated resources, we will not implement that functionality in this e-book.
So far we’ve implemented the Simple Awesome CRM system so that incidents are submitted and queried along with added comments and resources allocated—all of this in order to complete a job.
The ability to change properties for an existing document (IncidentInfo instance) is essential, as it might be necessary to change the IncidentStatus, IncidentSeverity, or any other property within a document.
Let’s implement the ChangePropertyValue method within the IncidentInfo class.
Code Listing 24: ChangePropertyValue Implementation
public async Task<IncidentInfo> ChangePropertyValue(string id, string propValue, string propName) { if (client == null) Connect2DocDb(); if (info != null && client != null) { var cases = from c in client.CreateDocumentQuery(docDbUrl) where c.Id.ToUpper().Contains(id.ToUpper()) select c; IncidentInfo issue = null; Document oDoc = null; foreach (var cs in cases) { var it = await client.ReadDocumentAsync(cs.AltLink); oDoc = it; issue = (IncidentInfo)(dynamic)it.Resource; break; } if (oDoc != null) { switch (propName) { case cStrSeverityProp: oDoc.SetPropertyValue(cStrSeverityProp, propValue); break; case cStrStatusProp: oDoc.SetPropertyValue(cStrStatusProp, propValue); break; case cStrFrequencyProp: oDoc.SetPropertyValue(cStrFrequencyProp, propValue); break; case cStrCTProp: oDoc.SetPropertyValue(cStrCTProp, propValue); break; case cStrClosedProp: oDoc.SetPropertyValue(cStrClosedProp, issue.Closed); break; } var updated = await client.ReplaceDocumentAsync(oDoc); issue = (IncidentInfo)(dynamic)updated.Resource; } return issue; } else return null; } |
Just as with AddComment and AddResource, ChangePropertyValue follows the same logic. It attempts to connect to DocumentDB through Connect2DocDb and by calling CreateDocumentQuery and querying for a specific ID. An IOrderedQueryable<Document> is returned for the ID of the document being queried.
The specific Document instance is then returned through a call to ReadDocumentAsync, and the respective property is changed using SetPropertyValue.
Next, the current Document is updated on the DocumentDB collection by calling ReplaceDocumentAsync.
For this to work—for it to be called from the main program—we’ll need to wrap up ChangePropertyValue in CrmExample, as shown in Code Listing 25.
Code Listing 25: ChangePropertyValue Wrapper
public static async void ChangePropertyValue(string id, string propValue, string propName) { using (Incident inc = new Incident()) { await Task.Run( async () => { IncidentInfo ic = await inc.ChangePropertyValue(id, propValue, propName); OutputCaseDetails(ic); }); } } |
As Code Listing 26 shows, this can then be invoked from the main program.
Code Listing 26: ChangePropertyValue Invocation
CrmExample.ChangePropertyValue("4b992d62-4750-47d2-ac4a-dbce2ce85c12", EnumUtils.stringValueOf(IncidentStatus.FeedbackRequested), "Status"); |
In this chapter, we’ve seen how to use DocumentDB and C# to implement a simple yet effective CRM API that you can mold to fit your needs.
Given its reliability, stability, robustness, and ease of use, DocumentDB is our preferred choice. Its API is easy to follow, needing very little documentation, and it fits perfectly with the concept of building a simple CRM API that allows us to cater to the concepts explained in Chapter 1.
The CRM sample code presented here is enough to get you started, and it should be easy to expand with added functionality.
You are invited to add your own ideas to this codebase and expand upon the concepts explained in Chapter 1—feel free to keep improving it.
In the next chapters, we’ll explore some ways that customer service can help your business grow and increase its value. We’ll also look at how simple reverse engineering can be a valuable asset for solving problems more quickly.