left-icon

Customer Success for C# Developers Succinctly®
by Ed Freitas

Previous
Chapter

of
A
A
A

CHAPTER 2

Incident Management

Incident Management


Introduction

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:

  • When a user expects a quick solution, your boss might want you to spend less time on that task and instead ask for a quick fix.
  • You must understand your limits and let all parties involved know what is reasonable.
  • If you are the main point of contact, you must let people know when you are out.
  • If you don’t have a full response or resolution immediately at your fingertips, a simple “no worries, I’ve got this” goes a long way.

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.

Simple, awesome CRM tools

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:

  • Allow the customer to report an incident along with their expectations (severity and importance).
  • Allow the customer to have one or multiple communication interactions.
  • Allow the customer to set a desired frequency level of communication interaction (bidirectional). This should alert the helpdesk to follow up or act.
  • Allow the helpdesk to report the state of the incident (as referred to in Table 2) until resolution, clearly indicating a probable delivery date for the fix.
  • Inform the customer clearly of the resources allocated (persons responsible) during each step and status.

Setting up DocumentDB

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.

Microsoft Azure Sign-In Screen

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.

DocumentDB within the List of Azure Services

Figure 2: DocumentDB within the List of Azure Services

After you select DocumentDB, you must create a new DocumentDB account by clicking Add.

Screen to Add a DocumentDB Account

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.

Final DocumentDB Account Creation Screen

Figure 4: Final DocumentDB Account Creation Screen

Figure 5 depicts how the DocumentDB account will appear after it is created.

DocumentDB Account Dashboard

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.

New DocumentDB Database Creation Screen

Figure 6: New DocumentDB Database Creation Screen

When the DocumentDB has been created, it will be configurable through an intuitive dashboard.

DocumentDB Database 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.

DocumentDB Internal Structure

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.

DocumentDB Collection’s Performance Levels and Pricing Tier

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.

Installing the .NET DocumentDB Client as a NuGet Package with Visual Studio 2015

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.

Simple CRM code

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

using Microsoft.Azure.Documents;

using Microsoft.Azure.Documents.Client;

using Microsoft.Azure.Documents.Linq;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Reflection;

using System.Threading.Tasks;

using CrmCore.EnumUtils;

using CrmCore.Enums;

using CrmCore.DateEpoch;

using CrmCore.IncidentInfo;

namespace CrmCore.Incident

{

    public class Incident : IDisposable

    {

        private bool disposed = false;

        private const string docDbUrl =

        "dbs/SimpleAwesomeCrm/colls/CrmObjects";

        private const string docDbEndpointUrl =

        "https://fastapps.documents.azure.com:443/";

        private const string docDbAuthorizationKey = "<<DocDb Key>>";

        private const string cStrSeverityProp = "Severity";

        private const string cStrStatusProp = "Status";

        private const string cStrFrequencyProp = "FeedbackFrequency";

        private const string cStrCTProp = "CommunicationType";

        private const string cStrClosedProp = "Closed";

        private const string cStrComments = "Comments";

        private const string cStrResources = "Resources";

        private const string cStrOpened = "Opened";

        public IncidentInfo info = null;

       

        // Class defined in Microsoft.Azure.Documents.Client

        protected DocumentClient client = null;

        ~Incident()

        {

            Dispose(false);

        }

        public Incident()

        {

            info = new IncidentInfo();

            info.Status =

              EnumUtils.stringValueOf(IncidentStatus.Reported);

            info.Severity =

              EnumUtils.stringValueOf(IncidentSeverity.Normal);

            info.FeedbackFrequency =

              EnumUtils.stringValueOf(IncidentFeedbackFrequency.Daily);

            info.CommunicationType =

              EnumUtils.stringValueOf(IncidentCommunicationType.

              ReceiveUpdatesOnly);

            info.Opened = new DateEpoch(DateTime.UtcNow);

            info.Resources = null;

            info.Comments = null;

        }

        public Incident(IncidentStatus status, IncidentSeverity severity,   

        IncidentFeedbackFrequency freq,

        IncidentCommunicationType comType)

        {

            info = new IncidentInfo();

            info.Status = EnumUtils.stringValueOf(status);

            info.Severity = EnumUtils.stringValueOf(severity);

            info.FeedbackFrequency = EnumUtils.stringValueOf(freq);

            info.CommunicationType = EnumUtils.stringValueOf(comType);

            info.Opened = new DateEpoch(DateTime.UtcNow);

            info.Resources = null;

            info.Comments = null;

        }

        private void Connect2DocDb()

        {

            client = new DocumentClient(new Uri(docDbEndpointUrl),

            docDbAuthorizationKey);

        }

        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;

        }

        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;

        }

        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;

        }

        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;

        }

        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;

        }

        protected virtual void Dispose(bool disposing)

        {

            if (!disposed)

            {

                if (disposing)

                    client.Dispose();

            }

            disposed = true;

        }

        public void Dispose()

        {

            Dispose(true);

            GC.SuppressFinalize(this);

        }

    }

}

Understanding incidents

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

using System;

using Newtonsoft.Json;

using System.Threading.Tasks;

using Microsoft.Azure.Documents;

using System.Linq;

using System.Collections.Generic;

using CrmCore.EnumUtils;

using CrmCore.Enums;

using CrmCore.DateEpoch;

using CrmCore.IncidentInfo;

using CrmCore.Incident;

namespace CrmCore

{

    public class CrmExample

    {

        private const string cStrCaseCreated =

        "Case created (as JSON) ->";

        private const string cStrTotalResults = "Total results: ";

        private const string cStrDescription = "Description: ";

        private const string cStrStatus = "Status: ";

        private const string cStrSeverity = "Severity: ";

        private const string cStrCommunication = "Communication: ";

        private const string cStrFrequency = "Frequency: ";

        private const string cStrOpened = "Opened: ";

        private const string cStrClosed = "Closed: ";

       

        private const string cStrDateTimeFormat =

        "dd-MMM-yyyy hh:mm:ss UTC";

        public static async Task<string> OpenCase(string description)

        {

            string id = string.Empty;

            using (Incident inc = new Incident(IncidentStatus.Reported,

                IncidentSeverity.High,

                IncidentFeedbackFrequency.Every4Hours,

                IncidentCommunicationType.Bidirectional))

            {

                var issue = await inc.Open(description);

                if (issue != null)

                {

                    var i = JsonConvert.DeserializeObject

                    <IncidentInfo>(issue.ToString());

                    Console.WriteLine(cStrCaseCreated);

                    Console.WriteLine(issue.ToString());

                    id = issue.Id;

                }

            }

            return id;

        }

        public static async Task<string> OpenCase(string description,

            IncidentStatus st, IncidentSeverity sv,

            IncidentFeedbackFrequency ff, IncidentCommunicationType ct)

        {

            string id = string.Empty;

            using (Incident inc = new Incident(st, sv, ff, ct))

            {

                var issue = await inc.Open(description);

                if (issue != null)

                {

                    var i = JsonConvert.DeserializeObject

                    <IncidentInfo>(issue.ToString());

                    Console.WriteLine(cStrCaseCreated);

                    Console.WriteLine(issue.ToString());

                    id = issue.Id;

                }

            }

            return id;

        }

       

        public static void CheckCase(string id)

        {

            using (Incident inc = new Incident())

            {

                IEnumerable<Document> issues = inc.FindById(id);

                foreach (var issue in issues)

                    OutputCaseDetails(issue);

            }

        }

        private static void OutputCaseDetails(object issue)

        {

            IncidentInfo i = (issue is IncidentInfo) ?

            (IncidentInfo)issue :

               JsonConvert.DeserializeObject

               <IncidentInfo>(issue.ToString());

            Console.WriteLine(cStrDescription + i?.Description);

            Console.WriteLine(cStrStatus + i?.Status);

            Console.WriteLine(cStrSeverity + i?.Severity);

            Console.WriteLine(cStrCommunication + i?.CommunicationType);

            Console.WriteLine(cStrFrequency + i?.FeedbackFrequency);

            Console.WriteLine(cStrOpened +

              i?.Opened?.Date.ToString(cStrDateTimeFormat));

            Console.WriteLine(cStrClosed +

              i?.Closed?.Date.ToString(cStrDateTimeFormat));

        }

    }

}

using CrmCore;

using System;

using System.Threading.Tasks;

namespace CustomerSuccess

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.Clear();

            OpenFindCase();

            Console.ReadLine();

        }

        public static async void OpenFindCase()

        {

            await Task.Run(

            async () =>

            {

                string id = await CrmExample.OpenCase(

                  "Export failing",

                  IncidentStatus.Reported,

                  IncidentSeverity.High,

                  IncidentFeedbackFrequency.Every8Hours,

                  IncidentCommunicationType.Bidirectional);

                Console.WriteLine(Environment.NewLine);

                CrmExample.CheckCase(id);

            });

        }

    }

}

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.

Incident Creation Console Output

Figure 12: Incident Creation Console Output

The document can be seen on Azure, as in Figure 13.

Incident in the Azure Document Explorer

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.

Find methods

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.

FindByDescription Results on Screen

Figure 14: FindByDescription Results on Screen

FindByDescription Results in Azure

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.

FindByDateOpenedAfter Results on Screen

Figure 16: FindByDateOpenedAfter Results on Screen

FindByDateOpenedAfter Results in Azure

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.

FindBySeverity Results on Screen

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.

Comments and resources

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.

AddComment Results in Azure

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

public static async void AddResource(string id, IncidentStatus stage,

    string engineer, DateTime st, DateTime end)

{

    using (Incident inc = new Incident())

    {

        await Task.Run(

        async () =>

        {

            IncidentInfo ic = await inc.AddResource(id, stage,

                engineer, st, end);

            OutputCaseDetails(ic);

        });

    }

}

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.

Changing properties

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");

Summary

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.