Lesson 2 - Create a RESTful API

Relativity exposes many REST endpoints, which you easily use to make API calls from various coding languages and tools, such as cURL or Postman. You make REST calls over HTTP, which provides language-agnostic programming and a high level of flexibility.

In this lesson, you will learn how to:

  • Use the Kepler framework to build RESTful APIs.
  • Implement a RESTful API which integrates with Wikipedia's public APIs.
  • Retrieve data in the required format.

Estimated completion time - 2 hours

Step 1 - Create an empty Kepler service

Begin by creating an empty Kepler service, which serves as the framework for the final service that you implement. (You will be using the Relativity Visual Studio templates that you installed in Set up a developer environment)

Use the following steps to create an empty Kepler service:

  1. Open Visual Studio.
  2. Click Create a new project.
    The Create a new project dialog appears.

    Create a new project button

  3. Select C# as the language for the project.
  4. Search for the Relativity Kepler Project Template.

    Create a new project dialog

  5. Click Next.
    The Configure your new project dialog appears.

    Configure your new project dialog

  6. Enter the following information in this dialog:
    • Project name - WikipediaKepler
    • Solution name - HelloWikipedia
    • Framework - .NET Framework 4.6.2
  7. Click Create.
    The Template Wizard appears.

    Template wizard

  8. Enter the following information in the wizard:
    • Service Module - WikipediaManagement
    • Service Name - WikipediaService
  9. Click Create.
    The wizard generates the solution and required projects.
  10. Verify that solution contains the WikipediaKepler.Interfaces and WikipediaKepler.Services projects, as illustrated in the following screen shot.

    Solution Explorer

    The projects are used as follows:

    • WikipediaKepler.Interfaces - add your API definitions to this project.
    • WikipediaKepler.Services - add your implementation to this project.
  11. Ensure that the generated API is compliant with REST best practices by navigating to WikipediaKepler.Interfaces > WikipediaManagement > IWikipediaManagementModule.cs in Visual Studio.
  12. Update the RoutePrefix attribute to wikipedia-management.
    Copy
    using Relativity.Kepler.Services;

    namespace WikipediaKepler.Interfaces.WikipediaManagement {
      /// <summary>
      /// WikipediaManagement Module Interface.
      /// </summary>
      [ServiceModule("WikipediaManagement Module")]
      [RoutePrefix("wikipedia-management", VersioningStrategy.Namespace)]
      public interface IWikipediaManagementModule {}
    }
  13. Navigate to WikipediaKepler.Interfaces > WikipediaManagement > v1 > IWikipediaService.cs in Visual Studio.
  14. Update the RoutePrefix attribute to wikipedia-service.
    Copy
    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Relativity.Kepler.Services;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1.Models;

    namespace WikipediaKepler.Interfaces.WikipediaManagement.v1 {
      /// <summary>
      /// MyService Service Interface.
      /// </summary>
      [WebService("WikipediaService Service")]
      [ServiceAudience(Audience.Public)]
      [RoutePrefix("wikipedia-service")]
      public interface IWikipediaService: IDisposable {
        /// <summary>
        /// Get workspace name.
        /// </summary>
        /// <param name="workspaceID">Workspace ArtifactID.</param>
        /// <returns><see cref="WikipediaServiceModel"/> with the name of the workspace.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [GET] /Relativity.REST/api/WIkipediaManagement/v1/WikipediaService/workspace/1015024
        /// Example REST response:
        ///   {"Name":"Relativity Starter Template"}
        /// </remarks>
        [HttpGet]
        [Route("workspace/{workspaceID:int}")]
        Task < WikipediaServiceModel > GetWorkspaceNameAsync(int workspaceID);

        /// <summary>
        /// Query for a workspace by name
        /// </summary>
        /// <param name="queryString">Partial name of a workspace to query for.</param>
        /// <param name="limit">Limit the number of results via a query string parameter. (Default 10)</param>
        /// <returns>Collection of <see cref="WikipediaServiceModel"/> containing workspace names that match the query string.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [POST] /Relativity.REST/api/WIkipediaManagement/v1/WikipediaService/workspace?limit=2
        ///   { "queryString":"a" }
        /// Example REST response:
        ///   [{"Name":"New Case Template"},{"Name":"Relativity Starter Template"}]
        /// </remarks>
        [HttpPost]
        [Route("workspace?{limit}")]
        Task < List < WikipediaServiceModel >> QueryWorkspaceByNameAsync(string queryString, int limit = 10);
      }
    }

    The final route is constructed based on the route prefixes that you just added:

    Copy
    Relativity.REST/api/{{IWikipediaManagementModule route prefix}}/{{IWikipediaService namespace version}}/{{IWikipediaService route prefix}}/{{IWikipediaService method route}}
  15. Build the solution.
    When the build completes, you have a functional Kepler service that you can deploy.

Step 2 - Deploy to Relativity

After implementing a functional Kepler service, you can upload it to Relativity and associate it with an application so that users can interact with it.

Use the following steps to deploy your service to Relativity:

  1. Log in to your Relativity instance.
  2. Click RelativityOne icon to display your home page.
  3. In the Sidebar, click Other Tabs > Applications & Scripts > Resource Files.
    The Resource Files tab appears.

    Resource Files option

  4. Click New Resource File.
    The Resource File Information dialog appears.

    Resource File Information dialog

  5. Click Select to display the Select Library Application dialog.
  6. Select the Hello Wikipedia application that you created in Lesson 1 - Build an application without any code.
  7. Click Choose File to select your compiled WikipediaKepler.Interfaces.dll to add as a new resource file.
  8. Click Save and New.
  9. Repeat steps 5 - 7 to add the WikipediaKepler.Services.dll, WikipediaKepler.Interfaces.pdb, and WikipediaKepler.Services.pdb as a resource files.

    You have now uploaded the .dlls and pdbs, associated them with the Hello Wikipedia application, and deployed them to Relativity.

Step 3 - Test the deployed service

You can test your new service after you have deployed it to Relativity.

Use the following steps to make a REST call:

  1. Make a REST request to your newly deployed Kepler service using an HTTP client such as Postman. If you get a 404 error, wait a few minutes and try again.
    • Method - POST
    • URL - use the following:
      Copy
      <host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/workspace
      • In the sample URL, <host> refers to the Relativity instance's base URL. On a test VM, the <host> value may look something like https://p-dv-vm-abc0efg
    • Headers - set the headers as follows:
      • X-CSRF-Header - set to a dash (-).
      • Authorization - set to Basic <basic-authorization-token>.

        Basic authentication is a simple authentication scheme built into the HTTP protocol. The client sends HTTP requests with the Authorization header that contains the word Basic followed by a space and a base64-encoded string, such as username:password. For example, if you wanted to authorize as user demo with the password p@55w0rd, use base64 to encode the password. Next, update the Authorization header to Basic with encoded password as ZGVtbzpwQDU1dzByZA==.

      • Content type - application/json
    • Body - add the following JSON:
      Copy
      {
        "queryString" : "My First Workspace"
      }
  2. Verify that you receive a successful response with the following payload:
    Copy
    [
     {
        "Name": "My First Workspace"
     }
    ]

Step 4 - Update the service

After confirming that your service is working, you can start updating it.

Use the following steps to update the service:

  1. Add a default value that the QueryWorkspaceByNameAsync() method returns by updating the WikipediaService.cs file with the following code for the QueryWorkspaceByNameAsync() method.
    Copy
    public async Task < List < WikipediaServiceModel >> QueryWorkspaceByNameAsync(string queryString, int limit) {
      var models = new List < WikipediaServiceModel > ();
      var unrealWorkspace = new WikipediaServiceModel {
        Name = "NotARealWorkspace"
      };

      models.Add(unrealWorkspace);
      // Create a Kepler service proxy to interact with other Kepler services.
      // Use the dependency injected IHelper to create a proxy to an external service.
      // This proxy will execute as the currently logged in user. (ExecutionIdentity.CurrentUser)
      // Note: If calling methods within the same service the proxy is not needed. It is doing so
      //       in this example only as a demonstration of how to call other services.
      var proxy = _helper.GetServicesManager().CreateProxy < IWikipediaService > (ExecutionIdentity.CurrentUser);

      // Validate queryString and throw a ValidationException (HttpStatusCode 400) if the string does not meet the validation requirements.
      if (string.IsNullOrEmpty(queryString) || queryString.Length > 50) {
        // ValidationException is in the namespace Relativity.Services.Exceptions and found in the Relativity.Kepler.dll.
        throw new ValidationException($"{nameof(queryString)} cannot be empty or grater than 50 characters.");
      }

      try {
        // Use the dependency injected IHelper to get a database connection.
        // In this example a query is made for all workspaces that are like the query string.
        // Note: async/await and ConfigureAwait(false) is used when making calls external to the service.
        //       See https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
        //       See also https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.configureawait
        //       See also https://blogs.msdn.microsoft.com/benwilli/2017/02/09/an-alternative-to-configureawaitfalse-everywhere/
        //       See also https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
        //       Warning: Improper use of the tasks can cause deadlocks and performance issues within an application.
        var workspaceIDs = await _helper.GetDBContext(-1).ExecuteEnumerableAsync(
          new ContextQuery {
            SqlStatement = @ "SELECT TOP (@limit) [ArtifactID] FROM [Case] WHERE [ArtifactID] > 0 AND [Name] LIKE '%'+@workspaceName+'%'",
              Parameters = new [] {
                new SqlParameter("@limit", limit),
                  new SqlParameter("@workspaceName", queryString)
              }
          }, (record, cancel) => Task.FromResult(record.GetInt32(0))).ConfigureAwait(false);

        foreach(int workspaceID in workspaceIDs) {
          // Loop through the results and use the proxy to call another service for more information.
          // Note: async/await and ConfigureAwait(false) is used when making calls external to the service.
          //       See https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
          //       See also https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.configureawait
          //       See also https://blogs.msdn.microsoft.com/benwilli/2017/02/09/an-alternative-to-configureawaitfalse-everywhere/
          //       See also https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
          //       Warning: Improper use of the tasks can cause deadlocks and performance issues within an application.
          WikipediaServiceModel wsModel = await proxy.GetWorkspaceNameAsync(workspaceID).ConfigureAwait(false);
          if (wsModel != null) {
            models.Add(wsModel);
          }
        }
      } catch (Exception exception) {
        // Note: logging templates should never use interpolation! Doing so will cause memory leaks.
        _logger.LogWarning(exception, "An exception occured during query for workspace(s) containing {QueryString}.", queryString);

        // Throwing a user defined exception with a 404 status code.
        throw new WikipediaServiceException($"An exception occured during query for workspace(s) containing {queryString}.");
      }

      return models;
    }
    Save the changes and rebuild the solution.
  2. Log in to your Relativity instance.
  3. Click RelativityOne icon to display your home page.
  4. In the Sidebar, click Other Tabs > Applications & Scripts > Resource Files.
    The Resource Files tab appears.

    Resource Files option

  5. In the Name filter, type WikipediaKepler.
    You should see the two .dlls and two .pdbs files that you uploaded in Step 2 - Deploy to Relativity.

    Resource File tab

  6. To update the implementation code, click WikipediaKepler.Services.dll.
    You only need to update these .dll and .pdb files because you didn't modify the interfaces.
  7. Click Edit.
    The Resource File Information dialog appears.
  8. Click Clear in the Resource File field.

    Image for Step 4 - Update the service

  9. Select your compiled WikipediaKepler.Services.dll to add as a new resource file.
  10. Click Save.
    The updated .dll is uploaded to Relativity and the service is redeployed.

    Note: If you want to remote debug this service, repeat steps 9-11 for the WikipediaKepler.Services.pdb file.

  11. After waiting a few minutes for the service to redeploy, repeat the REST call made in Step 3 - Test the deployed service.
    The same results should be returned along with the new default value.
    Copy
    [
      {
        "Name": "NotARealWorkspace"
      },
      {
        "Name": "My First Workspace"
      }
    ]

Step 5 - Implement methods on an interface

In this step, you implement each of the methods on the IWikipediaService interface in the IWikipediaService.cs file.

Use the following steps to add methods to the IWikipediaService interface:

  1. In Visual Studio, locate the IWikipediaService.cs file.
  2. Remove the existing methods in your IWikipediaService interface and in the WikipediaService class.
  3. Add the GetCategoriesByPrefixAsync() method to the IWikipediaService interface.
    Copy
    ...
    /// <summary>
    /// Returns a list of Categories in Wikipedia.
    /// </summary>
    /// <param name="prefix">Category prefix to limit results query of Categories in Wikipedia.</param>
    /// <returns><see cref="CategoryResponseModel"/> with the title of the category.</returns>
    /// <remarks>
    /// Example REST request:
    ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories?prefix=Star%20Wars
    /// Example REST response:
    ///   [{"Title":"Star Wars: The Rise of Skywalker"},{"Title":"Star Wars: A New Hope"}]
    /// </remarks>
    [HttpGet]
    [Route("categories?{prefix}")]
    Task<List<CategoryResponseModel>> GetCategoriesByPrefixAsync(string prefix);
    ...
  4. Add the CategoryResponseModel to the following file in your project: WikipediaKepler.Interfaces > WIkipediaManagement > v1 > Models > CategoryResponseModel.cs.
    Copy
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WikipediaKepler.Interfaces.WikipediaManagement.v1.Models {
      /// <summary>
      /// CategoryResponseModel Data Model.
      /// </summary>
      public class CategoryResponseModel {
        /// <summary>
        /// Title property.
        /// </summary>
        public string Title {
          get;
          set;
        }
      }
    }
  5. Add an interface to later inject in our WikipediaService. Add the IRestService to the following file in your project: WikipediaKepler.Interfaces > WIkipediaManagement > v1 > IRestService.cs
    Copy
    using System.Net.Http;
    using System.Threading.Tasks;

    namespace WikipediaKepler.Interfaces.WikipediaManagement.v1 {
      public interface IRestService {
        Task < HttpResponseMessage > GetAsync(string requestUri);
      }
    }
  6. Add the implementation WikipediaRestService to the following file in your project: WikipediaKepler.Services > WIkipediaManagement > v1 > WikipediaRestService.cs
    Copy
    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1;

    namespace WikipediaKepler.Services.WikipediaManagement.v1 {
      public class WikipediaRestService: IRestService {
        private static Lazy < HttpClient > _httpClient = new Lazy < HttpClient > (() => new HttpClient() {
          BaseAddress = new Uri("https://en.wikipedia.org/w/")
        });

        private HttpClient HttpClient => _httpClient.Value;

        public async Task < HttpResponseMessage > GetAsync(string requestUri) {
          return await HttpClient.GetAsync(requestUri);
        }
      }
    }
  7. Update the service constructor so that you can inject additional dependencies needed at runtime.
    In the WikipediaService() method, add the additional private fields and update the constructor as illustrated in the following code sample.

    With this update, you can avoid initializing other classes in the implementation, which increases the flexibility and reusability of the code. It also simplifies testing. The WikipediaService() method is in the WikipediaService.cs file.

    Copy
    ...
    private IRestService _restService;
    private
    const int _CATEGORY_TITLE_INDEX = 9;

    // Note: IHelper and ILog are dependency injected automatically into the constructor every time the service is called.
    public WikipediaService(IHelper helper, ILog logger, IRestService restService) {
        // Note: Set the logging context to the current class.
        _logger = logger.ForContext < WikipediaService > ();
        _helper = helper;
        _restService = restService;
      }
    ...
  8. Add a reference to System.Net.Http namespace to use the HttpClient class to both the WikipediaKepler.Interfaces and the WikipediaKepler.Services projects.
  9. Add the dependency injection code that injects IRestService at runtime. The classes extending IWindsorInstaller interface are injected and executed as part of the service deployment.

    Complete these steps:

    • Install the Castle.Windsor 3.3.0 NuGet package.
    • Add the WikipediaServiceInstaller class to the following file in your project: WikipediaKepler.Services > WIkipediaManagement > v1 > WikipediaServiceInstaller.cs.
    Copy
    using Castle.MicroKernel.Registration;
    using Castle.MicroKernel.SubSystems.Configuration;
    using Castle.Windsor;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1;

    namespace WikipediaKepler.Services.WikipediaManagement.v1 {
      public class WikipediaServiceInstaller: IWindsorInstaller {
        public void Install(IWindsorContainer container, IConfigurationStore store) {
          container.Register(Component.For < IRestService > ().ImplementedBy < WikipediaRestService > ().LifestyleTransient());
        }
      }
    }
  10. Review API:Allcategories on the MediaWiki site Before implementing GetCategoriesByPrefixAsync() method. Notice the request call and response format for retrieving a list of categories that match a specific prefix.
  11. Install the Newtonsoft.Json 6.0.8 NuGet package to use JSON as the response format.
  12. Add the following classes as internal models to simplify deserialization in the future implementation. These models are required for the implementation of the GetCategoriesByPrefixAsync() method.

    Add the models to the following folder in your project: WikipediaKepler.Services > WikipediaManagement > v1 > Models.

    • Add a WikipediaQueryResponse.cs file with this code:
      Copy
      namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
        internal class WikipediaQueryResponse {
          public Continue Continue {
            get;
            set;
          }
          public Query Query {
            get;
            set;
          }
        }
      }
    • Add a Continue.cs file with this code:
      Copy
      namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
        internal class Continue {
          public string CmContinue {
            get;
            set;
          }
        }
      }
    • Add a Query.cs file with this code:
      Copy
      using System.Collections.Generic;

      namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
        internal class Query {
          public Dictionary < string, Page > Pages {
            get;
            set;
          }
          public List < Item > CategoryMembers {
            get;
            set;
          }
        }
      }                    
    • Add a Page.cs file with this code:
      Copy
      using System.Collections.Generic;

      namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
        internal class Page: Item {
          public List < Item > Categories {
            get;
            set;
          }
          public string Extract {
            get;
            set;
          }
        }
      }                    
    • Add a Item.cs file with this code:
      Copy
      namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
        internal class Item {
          public string Title {
            get;
            set;
          }
        }
      }    
  13. Implement the GetCategoriesByPrefixAsync() method in the WikipediaService.cs file.
    Copy
    ...
    public async Task < List < CategoryResponseModel >> GetCategoriesByPrefixAsync(string prefix) {
        var categories = new List < CategoryResponseModel > ();
        HttpResponseMessage response = await _restService.GetAsync($"api.php?action=query&generator=allcategories&gacprefix={prefix}&prop=info&format=json");
        string content = await response.Content.ReadAsStringAsync();
        WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
        if (result.Query != null) {
          categories = result.Query.Pages.Values.Select(page => new CategoryResponseModel {
            Title = page.Title.Substring(_CATEGORY_TITLE_INDEX) // Substring to drop the 'Category:' prefix
          }).ToList();
        }
        return categories;
      }
    ...
  14. Add the GetPagesForCategoryAsync() method to the IWikipediaService interface in the IWikipediaService.cs file.
    Copy
    ...
    /// <summary>
    /// Get a list of pages under the provided Category in Wikipedia.
    /// </summary>
    /// <param name="categoryName">An existing Category in Wikipedia.</param>
    /// <param name="pageSize">Number of results in the page.</param>
    /// <param name="continueFrom">Identifier indicating where a paged result should be continued from. If '-', will start from the beginning.</param>
    /// <returns><see cref="CategoryResponseModel"/> with the title of the category.</returns>
    /// <remarks>
    /// Example REST request:
    ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories/Star%20Wars/pages?pageSize=10&continueFrom=page|123|456&pageSize=2
    /// Example REST response:
    ///   [{"Title":"Star Wars: The Rise of Skywalker"},{"Title":"Star Wars: A New Hope"}]
    /// </remarks>
    [HttpGet]
    [Route("categories/{categoryName}/pages?{pageSize}&{continueFrom}")]
    Task < Pageable < PageForCategoryResponseModel >> GetPagesForCategoryAsync(string categoryName, int pageSize = 10, string continueFrom = "-");
    ...
  15. Add models that are required to support paging for categories to the following folder in your project: WikipediaKepler.Interfaces > WIkipediaManagement > v1 > Models.
    • Add the Pageable.cs file with this code:
      Copy
      using System.Collections.Generic;

      namespace WikipediaKepler.Interfaces.WikipediaManagement.v1.Models {
        /// <summary>
        /// A generic container for pageable results
        /// </summary>
        /// <typeparam name="T">Type of results</typeparam>
        public class Pageable < T > {
          /// <summary>
          /// List of results of type <typeparam name="T"></typeparam>
          /// </summary>
          public List < T > Results {
            get;
            set;
          }

          /// <summary>
          /// Identifier for next page of results, if any. Can be empty.
          /// </summary>
          public string Next {
            get;
            set;
          }
        }
      }
    • Add the PageForCategoryResponseModel.cs file with this code:
      Copy
      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Threading.Tasks;

      namespace WikipediaKepler.Interfaces.WikipediaManagement.v1.Models {
          /// <summary>
          /// PageForCategoryResponseModel Data Model.
          /// </summary>
          public class PageForCategoryResponseModel {
            /// <summary>
            /// Title property.
            /// </summary>
            public string Title {
              get;
              set;
            }
          }
  16. Implement the GetPagesForCategoryAsync() method in the WikipediaService.cs file.
    It references the API:Categorymembers on the MediaWiki site.
    Copy
    ...
    public async Task < Pageable < PageForCategoryResponseModel >> GetPagesForCategoryAsync(string categoryName, int pageSize = 10, string continueFrom = "-") {
        var pages = new List < PageForCategoryResponseModel > ();
        continueFrom = continueFrom == null || continueFrom.Equals("-") ? string.Empty : continueFrom;
        HttpResponseMessage response = await _restService.GetAsync($"api.php?action=query&list=categorymembers&cmtitle=Category:{categoryName}&cmlimit={pageSize}&cmcontinue={continueFrom}&format=json");
        string content = await response.Content.ReadAsStringAsync();
        WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
        if (result.Query != null) {
          pages = result.Query.CategoryMembers.Select(item => new PageForCategoryResponseModel {
            Title = item.Title
          }).ToList();
        }
        string next = result.Continue?.CmContinue ?? string.Empty;

        return new Pageable < PageForCategoryResponseModel > {
          Results = pages,
          Next = next
        };
      }
    ...
  17. Implement the GetPageByNameAsync() method on the IWikipediaService interface by adding it to the IWikipediaService.cs file.
    Copy
    ...
    /// <summary>
    /// Returns an existing page in Wikipedia.
    /// </summary>
    /// <param name="pageName">Name of the page in Wikipedia.</param>
    /// <returns><see cref="PageResponseModel"/> with the Title, Url, and categories of the page.</returns>
    /// <remarks>
    /// Example REST request:
    ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars
    /// Example REST response:
    ///  {"Title":"Star Wars", "Url":"https://en.wikipedia.org/wiki/Star_Wars", "Categories":[{"Title":"Star Wars: The Rise of Skywalker"}]}
    /// </remarks>
    [HttpGet]
    [Route("pages/{pageName}")]
    Task < PageResponseModel > GetPageByNameAsync(string pageName);
    ...
  18. Implement the PageResponseModel class for use with the GetPageByNameAsync() method.
    This method requires a model used to return a standalone Page. Add the PageResponseModel.cs file to the WikipediaKepler.Interfaces > WIkipediaManagement > v1 > Models folder.
    Copy
    using System.Collections.Generic;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1.Models;

    namespace WikipediaKepler.Interfaces.WikipediaManagement.v1.Models {
      /// <summary>
      /// PageResponseModel Data Model.
      /// </summary>
      public class PageResponseModel {
        /// <summary>
        /// Title property.
        /// </summary>
        public string Title {
          get;
          set;
        }

        /// <summary>
        /// Url property.
        /// </summary>
        public string Url {
          get;
          set;
        }

        /// <summary>
        /// Categories property.
        /// </summary>
        public List < CategoryResponseModel > Categories {
          get;
          set;
        }
      }
    }
  19. Implement the GetPageByNameAsync() method in the WikipediaService.cs file.
    It references the API:Categories on the MediaWiki site.
    Copy
    ...
    public async Task < PageResponseModel > GetPageByNameAsync(string pageName) {
        HttpResponseMessage response = await _restService.GetAsync($"api.php?action=query&format=json&titles={pageName}&prop=categories");
        string content = await response.Content.ReadAsStringAsync();
        WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
        Page page = result.Query.Pages.First().Value;
        if (page.Categories == null) {
          string errorMsg = $ "Unable to find a page with name {pageName}.";
          _logger.LogError(errorMsg);
          throw new NotFoundException(errorMsg);
        }

        List < CategoryResponseModel > categories = page.Categories.Select(item => new CategoryResponseModel {
          Title = item.Title.Substring(_CATEGORY_TITLE_INDEX)
        }).ToList();
        return new PageResponseModel {
          Title = pageName,
            Url = $ "https://en.wikipedia.org/wiki/{Uri.EscapeUriString(pageName)}",
            Categories = categories
        };
      }
    ...
  20. Implement the GetPageTextAsync() method on the IWikipediaService interface by adding it to the IWikipediaService.cs file.
    Copy
    ...
    /// <summary>
    /// Returns a UTF-8 encoded text stream of an existing page in Wikipedia
    /// </summary>
    /// <param name="pageName">Name of the page in Wikipedia.</param>
    /// <returns>A <see cref="IKeplerStream"/> containing a UTF-8 encoded text stream from the specified page.</returns>
    /// <remarks>
    /// Example REST request:
    ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars/text
    /// Example REST response:
    ///  Star Wars is an American epic space-opera media franchise created by George Lucas[...]
    /// </remarks>
    [HttpGet]
    [Route("pages/{pageName}/text")]
    Task < IKeplerStream > GetPageTextAsync(string pageName);
    ...
  21. Implement the GetPageTextAsync() method in the WikipediaService.cs file.
    It references the API:Get the contents of a page on the MediaWiki site.
    Copy
    ...
    public async Task < IKeplerStream > GetPageTextAsync(string pageName) {
        HttpResponseMessage response = await _restService.GetAsync($"api.php?action=query&prop=extracts&titles={pageName}&format=json");
        string content = await response.Content.ReadAsStringAsync();
        WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
        Page page = result.Query.Pages.First().Value;
        if (page.Extract == null) {
          throw new NotFoundException($"Unable to find a page with name {pageName}.");
        }
        var stream = new MemoryStream(Encoding.UTF8.GetBytes(page.Extract));
        return new KeplerStream(stream) {
          ContentType = "text/html",
            StatusCode = HttpStatusCode.OK
        };
      }
      ...
  22. Verify that IWikipediaService interface is like the following code.

    This code sample lists all the methods on the IWikipediaService interface that following steps describe how to implement.

    Copy
    namespace WikipediaKepler.Interfaces.WikipediaManagement.v1 {
      /// <summary>
      /// Wikipedia Service Interface.
      /// </summary>
      [WebService("wikipedia-service Service")]
      [ServiceAudience(Audience.Public)]
      [RoutePrefix("wikipedia-service")]
      public interface IWikipediaService: IDisposable {
        /// <summary>
        /// Returns a list of Categories in Wikipedia.
        /// </summary>
        /// <param name="prefix">Category prefix to limit results query of Categories in Wikipedia.</param>
        /// <returns><see cref="CategoryResponseModel"/> with the title of the category.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories?prefix=Star%20Wars
        /// Example REST response:
        ///   [{"Title":"Star Wars: The Rise of Skywalker"},{"Title":"Star Wars: A New Hope"}]
        /// </remarks>
        [HttpGet]
        [Route("categories?{prefix}")]
        Task < List < CategoryResponseModel >> GetCategoriesByPrefixAsync(string prefix);

        /// <summary>
        /// Get a list of pages under the provided Category in Wikipedia.
        /// </summary>
        /// <param name="categoryName">An existing Category in Wikipedia.</param>
        /// <param name="pageSize">Number of results in the page.</param>
        /// <param name="continueFrom">Identifier indicating where a paged result should be continued from. If '-', will start from the beginning.</param>
        /// <returns><see cref="CategoryResponseModel"/> with the title of the category.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories/Star%20Wars/pages?pageSize=10&continueFrom=page|123|456&pageSize=2
        /// Example REST response:
        ///   [{"Title":"Star Wars: The Rise of Skywalker"},{"Title":"Star Wars: A New Hope"}]
        /// </remarks>
        [HttpGet]
        [Route("categories/{categoryName}/pages?{pageSize}&{continueFrom}")]
        Task < Pageable < PageForCategoryResponseModel >> GetPagesForCategoryAsync(string categoryName, int pageSize = 10, string continueFrom = "-");

        /// <summary>
        /// Returns an existing page in Wikipedia.
        /// </summary>
        /// <param name="pageName">Name of the page in Wikipedia.</param>
        /// <returns><see cref="PageResponseModel"/> with the Title, Url, and categories of the page.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars
        /// Example REST response:
        ///  {"Title":"Star Wars", "Url":"https://en.wikipedia.org/wiki/Star_Wars", "Categories":[{"Title":"Star Wars: The Rise of Skywalker"}]}
        /// </remarks>
        [HttpGet]
        [Route("pages/{pageName}")]
        Task < PageResponseModel > GetPageByNameAsync(string pageName);

        /// <summary>
        /// Returns a UTF-8 encoded text stream of an existing page in Wikipedia
        /// </summary>
        /// <param name="pageName">Name of the page in Wikipedia.</param>
        /// <returns>A <see cref="IKeplerStream"/> containing a UTF-8 encoded text stream from the specified page.</returns>
        /// <remarks>
        /// Example REST request:
        ///   [GET] /Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars/text
        /// Example REST response:
        ///  Star Wars is an American epic space-opera media franchise created by George Lucas[...]
        /// </remarks>
        [HttpGet]
        [Route("pages/{pageName}/text")]
        Task < IKeplerStream > GetPageTextAsync(string pageName);
      }
    }
  23. Verify that your final WikipediaService implementation class is like the following code.
    Copy
    using System.Net;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1.Models;
    using WikipediaKepler.Services.WikipediaManagement.v1.Models;

    namespace WikipediaKepler.Services.WikipediaManagement.v1 {
      public class WikipediaService: IWikipediaService {
        private IHelper _helper;
        private ILog _logger;

        private HttpClient _httpClient;
        private
        const int _CATEGORY_TITLE_INDEX = 9;

        // Note: IHelper and ILog are dependency injected automatically into the constructor every time the service is called.
        public WikipediaService(IHelper helper, ILog logger, HttpClient httpClient) {
          // Note: Set the logging context to the current class.
          _logger = logger.ForContext < WikipediaService > ();
          _helper = helper;
          _httpClient = httpClient;
          _httpClient.BaseAddress = new Uri("https://en.wikipedia.org/w/");
        }

        public async Task < List < CategoryResponseModel >> GetCategoriesByPrefixAsync(string prefix) {
          var categories = new List < CategoryResponseModel > ();
          HttpResponseMessage response = await _httpClient.GetAsync($"api.php?action=query&generator=allcategories&gacprefix={prefix}&prop=info&format=json");
          string content = await response.Content.ReadAsStringAsync();
          WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
          if (result.Query != null) {
            categories = result.Query.Pages.Values.Select(page => new CategoryResponseModel {
              Title = page.Title.Substring(_CATEGORY_TITLE_INDEX) // Substring to drop the 'Category:' prefix
            }).ToList();
          }
          return categories;
        }

        public async Task < Pageable < PageForCategoryResponseModel >> GetPagesForCategoryAsync(string categoryName, int pageSize = 10, string continueFrom = "-") {
          var pages = new List < PageForCategoryResponseModel > ();
          continueFrom = continueFrom == null || continueFrom.Equals("-") ? string.Empty : continueFrom;
          HttpResponseMessage response = await _httpClient.GetAsync($"api.php?action=query&list=categorymembers&cmtitle=Category:{categoryName}&cmlimit={pageSize}&cmcontinue={continueFrom}&format=json");
          string content = await response.Content.ReadAsStringAsync();
          WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
          if (result.Query != null) {
            pages = result.Query.CategoryMembers.Select(item => new PageForCategoryResponseModel {
              Title = item.Title
            }).ToList();
          }
          string next = result.Continue?.CmContinue ?? string.Empty;

          return new Pageable < PageForCategoryResponseModel > {
            Results = pages,
            Next = next
          };
        }

        public async Task < PageResponseModel > GetPageByNameAsync(string pageName) {
          HttpResponseMessage response = await _httpClient.GetAsync($"api.php?action=query&format=json&titles={pageName}&prop=categories");
          string content = await response.Content.ReadAsStringAsync();
          WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
          Page page = result.Query.Pages.First().Value;
          if (page.Categories == null) {
            string errorMsg = $ "Unable to find a page with name {pageName}.";
            _logger.LogError(errorMsg);
            throw new NotFoundException(errorMsg);
          }

          List < CategoryResponseModel > categories = page.Categories.Select(item => new CategoryResponseModel {
            Title = item.Title.Substring(_CATEGORY_TITLE_INDEX)
          }).ToList();
          return new PageResponseModel {
            Title = pageName,
              Url = $ "https://en.wikipedia.org/wiki/{Uri.EscapeUriString(pageName)}",
              Categories = categories
          };
        }

        public async Task < IKeplerStream > GetPageTextAsync(string pageName) {
          HttpResponseMessage response = await _httpClient.GetAsync($"api.php?action=query&prop=extracts&titles={pageName}&format=json");
          string content = await response.Content.ReadAsStringAsync();
          WikipediaQueryResponse result = JsonConvert.DeserializeObject < WikipediaQueryResponse > (content);
          Page page = result.Query.Pages.First().Value;
          if (page.Extract == null) {
            throw new NotFoundException($"Unable to find a page with name {pageName}.");
          }
          var stream = new MemoryStream(Encoding.UTF8.GetBytes(page.Extract));
          return new KeplerStream(stream) {
            ContentType = "text/html",
              StatusCode = HttpStatusCode.OK
          };
        }

        /// <summary>
        /// All Kepler services must inherit from IDisposable.
        /// Use this dispose method to dispose of any unmanaged memory at this point.
        /// See https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose for examples of how to properly use the dispose pattern.
        /// </summary>
        public void Dispose() {}
      }
    }        
  24. Try out the methods on your IWikipediaService interface.
    They communicate with the Wikipedia API to retrieve information. If you want to test them, upload your .dll and .pdb files again as in Step 2 - Deploy to Relativity.

Step 6 - Write a unit test

To shorten your development cycles, you can write unit tests that validate the functionality of a service. The following example includes only a single unit test, but you can write more unit tests to cover the functionality of your service.

Review these guidelines for writing unit tests:

  • Test a single scenario per test.
  • Mock any dependencies to ensure that the test is limited to only the behavior that you want to test.
  • Follow testing best practices for naming conventions.

Use the following steps to write a unit test:

  1. Open the HelloWikipedia solution in Visual Studio.
  2. Right-click on the solution and click Add > New Project.
    The Add a new project dialog appears.
  3. Select Unit Test Project (.NET Framework) and click Next.

    Add a new project dialog

  4. Enter the following information in the Configure your new project dialog:
    • Project name - WikipediaKepler.Tests
    • Location - set this field to the same root as your other projects.
    • Framework - verify that this field is set to .NET Framework 4.6.2.
  5. Click Create.
  6. To set up the required dependencies, right-click on the WikipediaKepler.Tests project, and click Add > Reference > Projects.
    The Reference Manager dialog appears.

    Reference Manager

  7. Select the checkboxes for the following projects:
    • WikipediaKepler.Interfaces
    • WikipediaKepler.Services
  8. Install the following NuGet packages:
    • NUnit
    • NUnit3TestAdapter
    • Moq
    • Newtonsoft.Json 6.0.8

      Note: Use Newtonsoft.Json 6.0.8 across all your projects for this tutorial. The matching versions prevent the tests from failing with IO exceptions.

    • Relativity.API 17.0.4
    • Relativity.Kepler 2.21.0
  9. Implement the WikipediaServiceTests class.
    As a best practice, mirror the directory structure of the class under test. Add it to the following file in your project: WikipediaKepler.Tests > WikipediaManagement > v1 > WikipediaServiceTests.cs.
    Copy
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Moq;
    using Moq.Protected;
    using Newtonsoft.Json.Linq;
    using NUnit.Framework;
    using Relativity.API;
    using Relativity.Kepler.Logging;
    using Relativity.Services.Exceptions;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1;
    using WikipediaKepler.Interfaces.WikipediaManagement.v1.Models;
    using WikipediaKepler.Services.WikipediaManagement.v1;

    namespace WikipediaKepler.Tests.WikipediaManagement.v1 {
      [TestFixture]
      public class WikipediaServiceTests {
        private Mock < IHelper > _helperMock;
        private Mock < ILog > _loggerMock;
        private Mock < IRestService > _restService;

        private
        const int _ONE_THOUSAND = 1000;
        private
        const string _WIDGETS = "Widgets";
        private readonly Random _rnd = new Random();

        [SetUp]
        public void SetUp() {
          _helperMock = new Mock < IHelper > ();
          _loggerMock = new Mock < ILog > ();
          _restService = new Mock < IRestService > ();
        }

        [TearDown]
        public void TearDown() {
          _helperMock = null;
          _loggerMock = null;
          _restService = null;
        }
      }
    }
  10. Implement a unit test for the GetCategoriesByPrefixAsync() method.
    The following code illustrates a basic unit test for the GetCategoriesByPrefixAsync() method in the WikipediaServiceTests.cs file.
    Copy
    ...
    private readonly Random _rnd = new Random();

    [Test]
    [Description("Verifies that when GetCategoriesByPrefixAsync is called a call is made to the Wikipedia API and matching categories are returned.")]
    public async Task GetCategoriesByPrefixAsync_MatchingPrefix_ReturnsMatchingCategories() {
        string prefix = "pre";
        string expectedCategory = $ "{prefix}determined";
        var responseJson = new JObject {
          ["batchcomplete"] = "",
          ["query"] = new JObject {
            ["pages"] = new JObject {
              ["123456"] = new JObject {
                ["pageid"] = _rnd.Next(0, _ONE_THOUSAND),
                  ["ns"] = _rnd.Next(0, _ONE_THOUSAND),
                  ["title"] = $ "Category:{expectedCategory}"
              }
            }
          }
        };
        var response = new HttpResponseMessage {
          StatusCode = HttpStatusCode.OK,
            Content = new StringContent(responseJson.ToString())
        };
        _restService.Setup(_ => _.GetAsync(It.IsAny < string > ())).ReturnsAsync(response);
        var service = new WikipediaService(_helperMock.Object, _loggerMock.Object, _restService.Object);
        List < CategoryResponseModel > actual = await service.GetCategoriesByPrefixAsync(prefix);
        Assert.That(actual.Count, Is.EqualTo(1));
        Assert.That(actual.First().Title, Is.EqualTo(expectedCategory));
      }
    ...
  11. Run the test and verify that it passes.
    Use this test as a model for adding coverage for edge cases, such as the behavior of your method when it receives an error from the Wikipedia API.
    To quickly run all the tests, open the Test Explorer window in Visual Studio and click Run All Tests, or in the toolbar, click Tests > Run All Tests.

Step 7 - Deploy and verify

After confirming that the interface successfully retrieves information from the Wikipedia API, and passes your local tests, you can add it to an application.

Use the following steps to add the interface to an application:

  1. Log in to your Relativity instance.
  2. Click RelativityOne icon to display your home page.
  3. In the Sidebar, click Other Tabs > Applications & Scripts > Resource Files.
    The Resource Files tab appears.

    Resource Files option

  4. Click New Resource File.
    The Resource File Information dialog appears.
  5. In the Name filter, type WikipediaKepler.
    You should see the two .dlls and the 2 pds files that you uploaded in Step 2 - Deploy to Relativity.

    Resource File tab

  6. Click WikipediaKepler.Services.dll to update the implementation code.
  7. Click Edit.
    The Resource File Information dialog appears.
  8. Click Clear in the Resource File field.

    Image for Step 7 - Deploy and verify

  9. Click Browse button to select your compiled WikipediaKepler.Services.dll to add as a new resource file.
  10. Click Save.
    The updated .dll is uploaded to Relativity and the service is redeployed.
  11. Repeat steps 5 - 10 for the WikipediaKepler.Interfaces.dll, WikipediaKepler.Services.pdb, and the WikipediaKepler.Interfaces.pdb files.
  12. Use the following sample REST calls to see how your service functions in a Relativity environment after waiting a few minutes for the service to redeploy.
    Your results might not exactly match the JSON in the expected results for these examples, because they execute against the live Wikipedia API and may change.
  13. Try out other API calls with your Kepler service.