Создание веб-сервиса WCF, который вызывает веб-сервис платформы
Создание веб-сервиса WCF, который вызывает веб-сервис платформы¶
Для интеграции с другими системами по протоколу SOAP или для различных других видов сервисов в SOA (Service Oriented Architecture) рекомендуется развернуть веб-сервер на платформе WCF (Windows Communication Foundation). Поскольку веб-сервис TESSA работает на платформе .NET, которая не имеет поддержки WCF, то вы можете создать отдельный веб-сервис WCF на .NET Framework, который взаимодействует с REST-сервисом TESSA (т.н. “микросервис”).
Сервис WCF возможно развернуть только на платформе .NET Framework под ОС Windows. При этом веб-сервис платформы TESSA может находиться на любом другом сервере, в т.ч. на Linux. Бизнес-логику такого сервиса мы рекомендуем перенести в контроллер и расширения в сервисе TESSA, а сервис WCF использовать в качестве “прослойки” между клиентом (системой, с которой выполняется интеграция) и веб-сервисом TESSA.
Note
Веб-сервис имеет параметры подключения только к веб-сервису TESSA по протоколу https. Он не должен иметь доступа к базе данных или файловым хранилищам системы.
Note
Для работы примера убедитесь, что у вас развёрнут веб-сервис для платформы актуальной версии (3.4 или старше), и в проектных расширениях Tessa.Extensions.Server есть ServiceController
в том виде, в каком он поставляется вместе со сборкой платформы (т.е. он не изменён в ваших расширениях).
Перед созданием проекта в Visual Studio
убедитесь, что установлен компонент Windows Communication Foundation
:
Создайте новый проект WCF Service Application
:
Укажите версию .NET Framework 4.7.2
, которая будет требоваться для работы разрабатываемого веб-сервиса. При необходимости вы можете указать более старшую версию .NET Framework 4.8
.
Нажмите пункт контекстного меню Manage NuGet Packages
в окне Solution Explorer
и установите NuGet-пакеты актуальных версий (не требуется синхронизировать версии, используемые в платформе, с версиями в вашем веб-сервисе):
-
Newtonsoft.Json
- используется для сериализации/десериализации объектов в формате JSON, который обычно применяется для REST-методов. -
System.Net.Http
- требуется для вызова REST-методов через APIHttpClient
.
В файле Web.config
укажите настройки подключения к веб-сервису платформы внутри тега <appSettings>
:
<appSettings>
<add key="BaseAddress" value="https://server/tessa"/>
<add key="SendTimeout" value="00:40:00"/>
<add key="Login" value="admin"/>
<add key="Password" value="admin"/>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>
-
BaseAddress
- базовый адрес сервиса, замените “server” на имя сервера или IP-адрес, а “tessa” - название папки в IIS (при развёртывании на Linux удалите). -
SendTimeout
- таймаут выполнения каждого запроса (в т.ч. таймаут открытия соединения). -
Login
- логин, который используется через свойствоServiceHelper.Login
в примерах контроллера для создания сессии в одном из методов. Для вашего сервиса указывать необязательно. -
Password
- пароль, который используется через свойствоServiceHelper.Password
в примерах контроллера для создания сессии в одном из методов. Для вашего сервиса указывать необязательно.
Для выполнения метода открытия сессии "service/Login"
в тестовом контроллере требуется сериализовать JSON-объект, аналогичный классу Tessa.Extensions.Shared.Services.IntegrationLoginParameters
. Объект содержит логин и пароль, для которых выполняется аутентификация.
Добавьте класс IntegrationLoginParameters.cs
:
namespace TessaWcfService
{
/// <summary>
/// Параметры входа в учётную запись для интеграционных сервисов.
/// </summary>
public class IntegrationLoginParameters
{
/// <summary>
/// Логин к учётной записи.
/// </summary>
public string Login { get; set; }
/// <summary>
/// Пароль к учётной записи.
/// </summary>
public string Password { get; set; }
}
}
Добавьте класс ServiceHelper.cs
со вспомогательными методами и свойствами. Вы можете поменять тестовый сервис IService1
под свои нужды, но методы и свойства класса ServiceHelper
вам, скорее всего, пригодятся:
using System;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace TessaWcfService
{
public static class ServiceHelper
{
private static volatile bool initialized;
private static readonly object syncObject = new object();
static ServiceHelper()
{
BaseAddress = ConfigurationManager.AppSettings["BaseAddress"];
if (string.IsNullOrEmpty(BaseAddress))
{
throw new InvalidOperationException("Specify base address in web.config");
}
string sendTimeout = ConfigurationManager.AppSettings["SendTimeout"];
SendTimeout = TimeSpan.Parse(sendTimeout, CultureInfo.InvariantCulture);
Login = ConfigurationManager.AppSettings["Login"];
Password = ConfigurationManager.AppSettings["Password"];
}
public static string BaseAddress { get; }
public static TimeSpan SendTimeout { get; }
public static string Login { get; }
public static string Password { get; }
public static void InitializeServicePointManager()
{
if (!initialized)
{
lock (syncObject)
{
if (!initialized)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
initialized = true;
}
}
}
}
public static HttpClient CreateHttpClient(bool windowsAuth = false)
{
InitializeServicePointManager();
return new HttpClient(
new HttpClientHandler
{
UseDefaultCredentials = windowsAuth,
PreAuthenticate = windowsAuth,
CheckCertificateRevocationList = false,
UseProxy = false
})
{
Timeout = SendTimeout
};
}
public static Uri GetRequestUri(string methodRoute) => new Uri(BaseAddress + "/web/" + methodRoute);
public static string TryGetContentType(HttpContent content)
{
HttpContentHeaders headers = content.Headers;
return headers.Contains("Content-Type") ? headers.ContentType.MediaType : null;
}
public static StringContent FromText(string text) =>
new StringContent(text ?? string.Empty, Encoding.UTF8, MediaTypeNames.Text.Plain);
public static StringContent FromJson(string text) =>
new StringContent(text, Encoding.UTF8, MediaTypeHeaderValue.Parse("application/json").MediaType);
public static StringContent FromJsonObject(object value)
{
var serializer = new JsonSerializer();
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
serializer.Serialize(writer, value ?? string.Empty);
}
return FromJson(sb.ToString());
}
public static async Task<string> SendAsync(
HttpMethod httpMethod,
string methodRoute,
HttpContent content = null,
string tokenHeader = null,
bool returnsVoid = false,
CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(tokenHeader))
{
if (content is null)
{
content = FromText(string.Empty);
}
content.Headers.Add("Tessa-Session", tokenHeader);
}
var request = new HttpRequestMessage(
httpMethod,
GetRequestUri(methodRoute))
{
Content = content,
Version = HttpVersion.Version11
};
using (HttpClient httpClient = CreateHttpClient())
using (HttpResponseMessage response = await httpClient.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
response.EnsureSuccessStatusCode();
string responseContentType = TryGetContentType(response.Content);
if (!string.IsNullOrEmpty(responseContentType))
{
responseContentType =
MediaTypeHeaderValue.TryParse(responseContentType, out MediaTypeHeaderValue header)
? header.MediaType
: null;
}
switch (responseContentType)
{
case null when returnsVoid || response.StatusCode == HttpStatusCode.NoContent:
return null;
case MediaTypeNames.Text.Plain:
case "application/json":
return await response.Content.ReadAsStringAsync();
default:
throw new NotSupportedException(
$"Unsupported response content type: \"{responseContentType}\"");
}
}
}
}
}
Содержимое интерфейса IService1.cs
замените таким образом:
using System;
using System.ServiceModel;
using System.Threading.Tasks;
namespace TessaWcfService
{
[ServiceContract]
public interface IService1
{
[OperationContract]
Task<string> CheckAsync();
[OperationContract]
Task<string> GetDataWithoutCheckingTokenAsync(string parameter);
[OperationContract]
Task<string> LoginAsync(string login, string password);
[OperationContract]
Task LogoutAsync(string token);
[OperationContract]
Task<string> GetCardAsync(Guid cardID, string cardTypeName = null);
}
}
В настоящем примере приводятся следующие методы WCF-сервиса:
-
CheckAsync
- посредством GET-запроса без аутентификации получает некий диагностический текст от сервиса платформы TESSA. -
GetDataWithoutCheckingTokenAsync
- выполняет POST-запрос без аутентификации на метод тестового контроллера ServiceController из расширений, который возвращает переданную ему строку, заодно выполнив запрос к базе данных TESSA. -
LoginAsync
- выполняет POST-запрос для создания сессии, возвращает токен созданной сессии. -
LogoutAsync
- выполняет POST-запрос и закрывает сессию по указанному в параметре токену, который можно получить, открыв сессию методомLoginAsync
. -
GetCardAsync
- возвращает структуру объектаCardGetResponse
для карточки с указанным идентификатором и опционально с заданным алиасом типа карточки; если алиас задан, то он помимо идентификатора дополнительно передаётся в запросCardGetRequest
.-
При этом сначала выполняется POST-запрос на создание сессии (аналогично
LoginAsync
), для этого используются параметры аутентификации вWeb.config
. -
Далее выполняется REST-запрос (POST) к тестовому контроллеру на получение структуры карточки по указанным идентификатору типа и алиасу типа карточки (параметры передаются через адресную строку). Также в этом запросе в HTTP-заголовках указывается header с именем
"Tessa-Session"
с токеном сессии в качестве значения (этот заголовок автоматически анализируется платформой в методах контроллеров). -
Проверяется актуальный тип карточки, полученный по указанному идентификатору, и если это тип
"Currency"
(валюта), то для него возвращается не полная структура карточки, а только название валюты, отображаемое пользователю. -
Перед выходом из метода в случае успешного выполнения или при наличии ошибок будет выполнен POST-запрос на закрытие сессии (аналогично методы
LogoutAsync
).
-
Описанные методы реализованы в классе WCF-сервиса Service1.svc
. Замените содержимое файла Service1.svc.cs
:
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json.Linq;
namespace TessaWcfService
{
public class Service1 : IService1
{
public async Task<string> CheckAsync() =>
await ServiceHelper.SendAsync(
HttpMethod.Get,
"service");
public async Task<string> GetDataWithoutCheckingTokenAsync(string parameter) =>
await ServiceHelper.SendAsync(
HttpMethod.Post,
"service/GetDataWithoutCheckingToken",
ServiceHelper.FromText(parameter));
public async Task<string> LoginAsync(string login, string password) =>
await ServiceHelper.SendAsync(
HttpMethod.Post,
"service/Login",
ServiceHelper.FromJsonObject(
new IntegrationLoginParameters { Login = login, Password = password }));
public async Task LogoutAsync(string token) =>
await ServiceHelper.SendAsync(
HttpMethod.Post,
"service/Logout",
ServiceHelper.FromText(token),
returnsVoid: true);
public async Task<string> GetCardAsync(Guid cardID, string cardTypeName = null)
{
string token = await this.LoginAsync(ServiceHelper.Login, ServiceHelper.Password);
try
{
string jsonResponse = await ServiceHelper.SendAsync(
HttpMethod.Post,
$"service/card/{cardID}"
+ (string.IsNullOrEmpty(cardTypeName) ? null : $"?type={HttpUtility.UrlEncode(cardTypeName)}"),
tokenHeader: token);
dynamic response = JObject.Parse(jsonResponse);
if ((string) response.Card.TypeName == "Currency")
{
string currencyCaption = response.Card.Sections.Currencies.Fields.Caption;
return currencyCaption;
}
return response;
}
finally
{
await this.LogoutAsync(token);
}
}
}
}
Убедитесь, что сервис TESSA доступен по параметрам, указанным в файле Web.config
. Теперь выберите в дереве Solution Explorer (в Visual Studio ) файл Service1.svc
или Service1.svc.cs
, и нажмите F5
. Это запустит веб-сервис в IIS Express, и также откроет WCF Test Client
- приложение для тестирования WCF-сервисов. В нём мы можем проверить работу сервисов.
Дважды кликните на методе (который не отмечен красным крестом - не обращайте внимание на такие методы). Например, на скриншоте указан метод GetCard
(это метод IService1.GetCardAsync
), и для него в параметре cardID
задан идентификатор карточки валюты RUS
, которая присутствует в типовой поставке платформы. Метод вернул строку с отображаемым названием валюты, которую он получил от веб-сервиса TESSA.
Tip
При возникновении ошибок можно поставить точку останова в коде сервиса Service1.svc.cs
, чтобы выяснить причину ошибки.
Т.о. можно создавать как WCF-приложения, так и любые другие серверные приложения (веб-сервисы, фоновые сервисы, консольные утилиты), которые обращаются к веб-сервису TESSA, открывают сессию и выполняют по ней работу. Такие приложения могут быть написаны на платформах .NET Framework или .NET Core, в этом случае можно использовать API HttpClient
аналогичным образом. Возможно аналогичное использование REST API на других языках и платформах.