

Last date modified: June 17 2025
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:
Estimated completion time - 2 hours
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:
The projects are used as follows:
1
2
3
4
5
6
7
8
9
10
using Relativity.Kepler.Services;
namespace WikipediaKepler.Interfaces.WikipediaManagement {
/// <summary>
/// WikipediaManagement Module Interface.
/// </summary>
[ServiceModule("WikipediaManagement Module")]
[RoutePrefix("wikipedia-management", VersioningStrategy.Namespace)]
public interface IWikipediaManagementModule {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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:
1
Relativity.REST/api/{{IWikipediaManagementModule route prefix}}/{{IWikipediaService namespace version}}/{{IWikipediaService route prefix}}/{{IWikipediaService method route}}
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:
You have now uploaded the .dlls and pdbs, associated them with the Hello Wikipedia application, and deployed them to Relativity.
You can test your new service after you have deployed it to Relativity.
Use the following steps to make a REST call:
1
<host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/workspace
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==.
1
2
3
{
"queryString" : "My First Workspace"
}
1
2
3
4
5
[
{
"Name": "My First Workspace"
}
]
After confirming that your service is working, you can start updating it.
Use the following steps to update the service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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;
}
If you want to remote debug this service, repeat steps 9-11 for the WikipediaKepler.Services.pdb file.
1
2
3
4
5
6
7
8
[
{
"Name": "NotARealWorkspace"
},
{
"Name": "My First Workspace"
}
]
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
/// <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);
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}
}
1
2
3
4
5
6
7
8
using System.Net.Http;
using System.Threading.Tasks;
namespace WikipediaKepler.Interfaces.WikipediaManagement.v1 {
public interface IRestService {
Task < HttpResponseMessage > GetAsync(string requestUri);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
...
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;
}
...
Complete these steps:
1
2
3
4
5
6
7
8
9
10
11
12
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());
}
}
}
Add the models to the following folder in your project: WikipediaKepler.Services > WikipediaManagement > v1 > Models.
1
2
3
4
5
6
7
8
9
10
11
12
namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
internal class WikipediaQueryResponse {
public Continue Continue {
get;
set;
}
public Query Query {
get;
set;
}
}
}
1
2
3
4
5
6
7
8
namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
internal class Continue {
public string CmContinue {
get;
set;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}
1
2
3
4
5
6
7
8
namespace WikipediaKepler.Services.WikipediaManagement.v1.Models {
internal class Item {
public string Title {
get;
set;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
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;
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
/// <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 = "-");
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
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
};
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
/// <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);
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
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
};
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
/// <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);
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
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
};
}
...
This code sample lists all the methods on the IWikipediaService interface that following steps describe how to implement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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() {}
}
}
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:
Use the following steps to write a unit test:
Use Newtonsoft.Json 6.0.8 across all your projects for this tutorial. The matching versions prevent the tests from failing with IO exceptions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
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));
}
...
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
<host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories?prefix=Star%20Wars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
{
"Title": "Star Wars"
},
{
"Title": "Star Wars: Battlefront"
},
{
"Title": "Star Wars: Episode III – Revenge of the Sith"
},
{
"Title": "Star Wars: Episode III – Revenge of the Sith video games"
},
{
"Title": "Star Wars: Episode II – Attack of the Clones"
},
{
"Title": "Star Wars: Episode II – Attack of the Clones video games"
},
{
"Title": "Star Wars: Episode I – The Phantom Menace"
},
{
"Title": "Star Wars: Episode I – The Phantom Menace video games"
},
{
"Title": "Star Wars: Galaxy's Edge"
},
{
"Title": "Star Wars: Jedi Apprentice"
}
]
1
<host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/categories/Star%20Wars/pages?pageSize=5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Results": [
{
"Title": "Charley Lippincott"
},
{
"Title": "Star Wars"
},
{
"Title": "Clone Wars (Star Wars)"
},
{
"Title": "Comparison of Star Trek and Star Wars"
},
{
"Title": "Death Star (business)"
}
],
"Next": "page|313f4d4f4b3131044d4f512f39454d011301dcbfdc0a|594188"
}
1
<host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"Title": "Star Wars",
"Url": "https://en.wikipedia.org/wiki/Star%20Wars",
"Categories": [
{
"Title": "20th Century Fox films"
},
{
"Title": "Action film franchises"
},
{
"Title": "Adventure film series"
},
{
"Title": "All Wikipedia articles written in American English"
},
{
"Title": "All articles needing additional references"
},
{
"Title": "All articles with unsourced statements"
},
{
"Title": "American epic films"
},
{
"Title": "American science fantasy films"
},
{
"Title": "Articles needing additional references from January 2020"
},
{
"Title": "Articles with short description"
}
]
}
1
<host>/Relativity.REST/api/wikipedia-management/v1/wikipedia-service/pages/Star%20Wars/text
1
<Displays the plain text content of the Star Wars Wikipedia entry.>
Why was this not helpful?
Check one that applies.
Thank you for your feedback.
Want to tell us more?
Great!
Additional Resources |
|||
DevHelp Community | GitHub | Release Notes | NuGet |