Виджет CDEK с бэкендом на .NET
Всем привет. Некоторое время назад стояла задача интегрировать виджет CDEK в сайт на .NET. Код виджета доступен на github: фронт (ts) + бэкенд (php). При переносе на .NET с фронтом проблем нет. Кроме того, есть готовый скрипт, который можно подключить с cdn. Но при этом есть существенное ограничение для бэкенда: данный скрипт работает только с php.
В данной статье показано, как перевести виджет на бэкенд .NET. При этом фронтенд код остается неизменным. Прежде, чем начать, хочу предупредить, что данное решение никак не связано с официальной версией виджета и не поддерживается командой CDEK. В любой момент CDEK может изменить свой код без сохранения обратной совместимости, и решение, представленное здесь, может перестать работать. Тем не менее, думаю (вернее, мне хотелось бы так думать :)), что информация, представленная в данной статье, может быть полезной как с точки зрения конечного результата, так и в образовательных целях.
По умолчанию фронт виджета посылает запросы на файл service.php (оригинальный код), находящийся в корне сайта. Поэтому задача в конечном счете свелась к двум подзадачам:
добавить имитацию service.php для запросов с фронта в .NET сайт
перевести код service.php на .NET
Первая подзадача решается достаточно просто через добавление промежуточного слоя (middleware) для запросов на service.php. В типовом ASP.NET Core приложении это можно сделать, добавив следующий код в Program.cs:
app.UseWhen(
context => context.Request.Path.ToString().ToLower().Contains("/service.php"),
appBranch => {
appBranch.UseCdekMiddleware();
});
С данной конструкцией запросы на service.php будут обрабатываться нашим промежуточным слоем CdekMiddleware (обращаю внимание, что все это мы делаем внутри .NET сайта, не имеющим ничего общего с php. Т.е. php не нужно устанавливать на ваш сервер, где крутится .NET сайт). Соответственно код метода-расширения UseCdekMiddleware () и самого промежуточного слоя:
internal class CdekMiddleware
{
public CdekMiddleware(RequestDelegate next)
{
}
public async Task Invoke(HttpContext context, ICdekService cdekService)
{
var requestParams = await this.getRequestParams(context);
object action = null;
requestParams?.TryGetValue("action", out action);
if (string.IsNullOrEmpty(action as string))
{
await this.sendValidationError(context, "Action is required");
return;
}
if (string.Compare(action as string, "offices", true) == 0)
{
var result = cdekService.GetOffices(requestParams);
await this.writeResponseAsync(context, result);
return;
}
if (string.Compare(action as string, "calculate", true) == 0)
{
var calculations = cdekService.Calculate(requestParams);
await this.writeResponseAsync(context, calculations);
return;
}
await this.sendValidationError(context, "Unknown action");
}
private async Task> getRequestParams(HttpContext ctx)
{
var data = new Dictionary();
foreach (var kv in ctx.Request.Query)
{
data[kv.Key] = kv.Value.ToString();
}
var stream = ctx.Request.Body;
string json = await new StreamReader(stream).ReadToEndAsync();
if (!string.IsNullOrEmpty(json))
{
var body = JsonConvert.DeserializeObject(json) as IDictionary;
body?.ToList().ForEach(kv => { data[kv.Key] = kv.Value; });
}
return data;
}
private async Task sendValidationError(HttpContext context, string msg)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await this.writeResponseAsync(context, new CdekResponse{Data = msg});
}
private async Task writeResponseAsync(HttpContext context, CdekResponse resp)
{
resp?.Headers?.ToList().ForEach(kv =>
{
context.Response.Headers.TryAdd(kv.name, kv.value);
});
await context.Response.WriteAsJsonAsync((object)resp?.Data);
}
}
internal static class CdeknMiddlewareExtension
{
public static IApplicationBuilder UseCdekMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
При переносе кода старался сделать его как можно ближе к оригиналу, используя мои (скромные) знания php. Как видно из анализа кода оригинального виджета, service.php по сути является прокси между фронтом и API CDEK, который работает в вашем домене, чтобы при подключении фронта не было проблем с CORS. Поэтому вместе непосредственно с данными в формате json, в ответ сервера также добавляются кастомные HTTP заголовки, пришедшие из API (заголовки, названия которых начинаются с X-). Так например виджет получает список пунктов вывоза с поддержкой постраничой навигации (используются заголовки X-Current-Page, X-Total-Elements, X-Total-Pages).
Внутри CdekMiddleware вызывает CdekService для получения списка пунктов вывоза и расчета стоимости доставки. Код CdekService вместе с рабочим тестовым приложением на .NET8 доступен на github. Инструкция для запуска приложения также найдете на github. Для запуска вам потребуюся clientId/clientSecret для API CDEK (тестовые ключи можно найти на сайте документации CDEK) и ключ API Яндекс Карт (необходимо сгенерировать в кабинете разработчика Яндекс).
Настройки виджета в приложении взяты из примера CDEK с указанием всех настроек виджета. Если все сделано правильно, то при запуске появится виджет с возможностью выбора пункта вывоза и расчета стоимости доставки:

