Article Exposing Data to External Sources

When creating new content, applications, and/or other custom data and services within Verint Community, you can expose those services for external integration via REST. This offers immense flexibility in allowing your custom data to be accessed  by external integrations.

When Should I Use REST Endpoints?

Anytime you want to enable applications outside of Verint Community to interact with your custom data, you should expose the data via REST. This can also be used in the Verint Community UI for AJAX operations within widgets.  Additionally you can wrap one or more In-Process APIs into a new REST endpoint if you wanted to provide a broader response or even limit the response to specific fields.

Our example will illustrate our standard recommended REST endpoint set for entities: Create, Update, Delete, Get, and List.  You are not required to use them all if your scenario doesn't need them.

Required DLL references:

  • Telligent.Evolution.Components
  • Telligent.Evolution.Rest

Sample Content

The first step in the process is to have custom data or service written to create REST endpoints against.  While this article focuses on custom data, you could also create a custom object to store community data that is retrieved from one or more in-process APIs and return that.  In this example we will focus on a custom class called "Dog".

using System;

namespace Samples
{
    public class Dog
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Breed { get; set; }
    }
}

For the sake of this demo, we assume a supporting service exists to perform CRUD operations on this object. We refer to it as DogService and assume its methods take all required parameters individually, and group all optional parameters into an Dog<MethodName>Options class.  This is the same pattern used for our In-Process API.

Registering REST Routes

Implementing IRestEndpoints requires a Register() method that provides you with a controller with which to register your REST routes. Routes will always start with the standard Verint Community REST URL (<siteurl> + "/api.ashx/{version}"); we will define what the rest of the url should look like including the version for each desired method. IRestEndpoints is also derived from IPlugin, so you will need to implement the basic IPlugin interface as well.

The declaration of the endpoint is actually fairly simple. The parameters are:

  • int version: This allows you to have multiple versions of an endpoint at the same URL.  This allows you to change an API but maintain some backwards compatibility if you so choose.   This value is also used in the root of the url after api.ashx.  So for example if you set this to 1, then the base url for your endpoint is http://<siteurl>/api.ashx/v1, if its 2 then /api.ashx/v2 and so forth.
  • string relativeUrl: This is the addition to the base REST URL(<siteurl> + "/api.ashx/{version}") to identify this method to the handler. Together with the HTTP method and any parameter constraints, this makes the url unique.
  • object parameterDefaults: Specifies default settings about the routes, such as what the REST resource name is and what REST action is being taken.  It can also be used to set default values for any parameters that are optional.
  • object parameterConstraints: Constraints would be used to specify things like the required format of parameters in the route url.
  • HttpMethod method: Corresponds to the standard HTTP methods (GET, POST, PUT, DELETE). Here we use the HttpMethod enum.
  • Func<IRestRequest, IRestResponse> handler: This is the delegate that will be invoked to handle the request and issue a response. It can either be a defined method or an inline Func<> declaration.

public void Register(IRestEndpointController restRoutes)
{
    restRoutes.Add(1, "dogs", new { resource = "dog", action = "create" }, null, HttpMethod.Post, CreateDog);
    restRoutes.Add(1, "dog/{id}", new { resource = "dog", action = "update" }, null, HttpMethod.Put, UpdateDog);
    restRoutes.Add(1, "dog/{id}", new { resource = "dog", action = "delete" }, null, HttpMethod.Delete, DeleteDog);
    restRoutes.Add(1, "dog/{id}", new { resource = "dog", action = "show" }, null, HttpMethod.Get, GetDog);
    restRoutes.Add(1, "dogs", new { resource = "dog", action = "list" }, null, HttpMethod.Get, ListDogs);
}

Declaring route urls is similar to MVC Routing in that the routes can accidentally overlap, hiding some routes completely. You need to ensure that this does not happen so that all your methods will be available and that calls to those methods do not get routed to the wrong internal methods. You can set routes apart by a combination of HTTP method, parameter constraints, and the route itself. A few cases to look out for and avoid regarding overlapping routes:

  • Matching Routes: This is safe as long as the HTTP method varies. For instance, Update, Delete, and Get all use the same route ("dog/{id}"), but are differentiated using the HTTP method (PUT, DELETE, and GET respectively).
  • Matching Http Methods: This is safe as long as the routes are unique. Get and List have the same HTTP Method (GET) , so they need different route urls to differentiate them ("dog/{id}" and "dogs").
  • Matching route parameter tokens: Care needs to be taken when routes are similar to those with parameter tokens in them. For instance, if a route "dog/{id}" (mapping to a Get method) was declared prior to a route "dog/create" (mapping to a Create method), without a parameter constraint on {id} specifying it had to be something other than a string, "create" could be interpreted as a match to the {id} token, causing "dog/create" to trigger the Get method with an invalid id.

Each Rest method accepts an IRestRequest object and returns an object that implements IRestResponse. There are two main components of the request that store data; PathParameters and Form. The contents of PathParameters are what is parsed from the url path, including the query string; the contents of Form are parsed from the request body. You can also choose to access the query string directly from IRestRequest.Request.QueryString. 

The IRestResponse interface is fairly generic, but allows you a location to put your data object so that it can be serialized into the HTTP response. It also requires a name for your data object (for the outermost serialized entity) and provides a list for Error strings to be included in the response.

The functions for performing each action are declared separately. First, we must create a Response object that implements IRestResponse so that we can send the data in a known format.

[XmlRoot(ElementName = "Response")]
[RestAction(RestAction.Show, "achievement")]
public class DogResponse : IRestResponse
{
    private Dog _dog;
    public Dog Dog
    {
        get { return _dog; }
        set
        {
            _dog = value;
        }
    }

    public string Name { get { return "Dog"; } }
    public object Data { get { return _dog; } }
    public string[] Errors { get; set; }
}
 

Create is used when we want to add an instance of the object. In this example, adding a dog requires just the Name, so we check for that and return early with an error if missing. Additional checks could be made here, for instance if a required parameter needed to be an integer. Checking for optional parameters is easier; we only need to assign it to the options object if it does exist. Once all the information you need is collected, you can make a call to your internal service, perform the necessary operations, and return relevant information in the REST response. It is important to wrap the internal calls in a try...catch block so you can ensure you return a REST response to the request, and not an ASP.net error page. If a required parameter is missing or does not fulfill the requirements, or another problem occurs internally, an error response can be returned (It is recommended to use an IUserRenderableException for this). If all required parameters are included, each can be parsed and then used with the underlying internal service to perform the operation requested.

public IRestResponse CreateDog(IRestRequest request)
{
    var response = new DogResponse();

    if (request.Form["Name"] == null)
    {
        response.Errors = new[] { "Name is required" };
        return response;
    }
    var name = request.PathParameters["Name"].ToString();

    var options = new DogCreateOptions();

    if (request.Form["Breed"] != null)
        options.Breed = request.Form["Breed"];

    try
    {
        var createdDog = DogService.Create(name, options);

        response.Dog = createdDog;
    }
    catch (Exception ex)
    {
        response.Errors = new[] { "Error: '{0}'", ex.ToString() };
    }

    return response;
}

Update uses the Id originally returned from Create to find the object, which allows all other parameters to be optional and only change if specified.

public IRestResponse UpdateDog(IRestRequest request)
{
    var response = new DogResponse();

    if (!request.PathParameters.ContainsKey("Id"))
    {
        response.Errors = new[] { "Id is required" };
        return response;
    }
    var id = Guid.Parse(request.PathParameters["Id"].ToString());

    var options = new DogUpdateOptions();

    if (request.Form["Name"] != null)
        options.Name = request.PathParameters["Name"].ToString();
    if (request.Form["Breed"] != null)
        options.Breed = request.PathParameters["Breed"].ToString();

    try
    {
        var createdDog = DogService.Update(id, options);

        response.Dog = createdDog;
    }
    catch (Exception ex)
    {
        response.Errors = new[] { "Error: '{0}'", ex.ToString() };
    }

    return response;
}

Delete only needs the Id of the object to remove it from the database.

public IRestResponse DeleteDog(IRestRequest request)
{
    var response = new DefaultRestResponse();

    if (!request.PathParameters.ContainsKey("Id"))
    {
        response.Errors = new[] { "Id is required" };
        return response;
    }
    var id = Guid.Parse(request.PathParameters["Id"].ToString());

    try
    {
        DogService.Delete(id);
    }
    catch (Exception ex)
    {
        response.Errors = new[] { "Error: '{0}'", ex.ToString() };
    }

    return response;
}

Get also needs only the Id to return the full object.

public IRestResponse GetDog(IRestRequest request)
{
    var response = new DogResponse();

    var options = new DogGetOptions();

    if (!request.PathParameters.ContainsKey("Id"))
    {
        response.Errors = new[] { "Id is required" };
        return response;
    }
    options.Id = Guid.Parse(request.PathParameters["Id"].ToString());

    try
    {
        var dog = DogService.Get(options);
        response.Dog = dog;
    }
    catch (Exception ex)
    {
        response.Errors = new[] { "Error: '{0}'", ex.ToString() };
    }

    return response;
}

List is the most flexible method in terms of what can be returned from it, and therefore includes no required parameters but many additional parameters for customizing the returned list.

public IRestResponse ListDogs(IRestRequest request)
{
    var response = new DefaultRestResponse();
    var options = new DogListOptions();

    if (request.PathParameters.ContainsKey("Name"))
        options.Name = request.PathParameters["Name"].ToString();
    if (request.PathParameters.ContainsKey("Breed"))
        options.Breed = request.PathParameters["Breed"].ToString();

    if (request.PathParameters.ContainsKey("SortBy"))
        options.SortBy = request.PathParameters["SortBy"].ToString();
    if (request.PathParameters.ContainsKey("SortOrder"))
        options.SortOrder = request.PathParameters["SortOrder"].ToString();

    if (request.PathParameters.ContainsKey("PageSize"))
        options.PageSize = Int32.Parse(request.PathParameters["PageSize"].ToString());
    if (request.PathParameters.ContainsKey("PageIndex"))
        options.PageIndex = Int32.Parse(request.PathParameters["PageIndex"].ToString());

    try
    {
        var dogs = DogService.List(options);

        response.Name = "Dogs";
        response.Data = dogs;
    }
    catch (Exception ex)
    {
        response.Errors = new[] { "Error: '{0}'", ex.ToString() };
    }

    return response;
}

Using the Registered Routes

For information on making calls to REST end points, see REST API: Making Requests.