Виджет CDEK с бэкендом на .NET

Всем привет. Некоторое время назад стояла задача интегрировать виджет CDEK в сайт на .NET. Код виджета доступен на github: фронт (ts) + бэкенд (php). При переносе на .NET с фронтом проблем нет. Кроме того, есть готовый скрипт, который можно подключить с cdn. Но при этом есть существенное ограничение для бэкенда: данный скрипт работает только с php.

В данной статье показано, как перевести виджет на бэкенд .NET. При этом фронтенд код остается неизменным. Прежде, чем начать, хочу предупредить, что данное решение никак не связано с официальной версией виджета и не поддерживается командой CDEK. В любой момент CDEK может изменить свой код без сохранения обратной совместимости, и решение, представленное здесь, может перестать работать. Тем не менее, думаю (вернее, мне хотелось бы так думать :)), что информация, представленная в данной статье, может быть полезной как с точки зрения конечного результата, так и в образовательных целях.

По умолчанию фронт виджета посылает запросы на файл service.php (оригинальный код), находящийся в корне сайта. Поэтому задача в конечном счете свелась к двум подзадачам:

  1. добавить имитацию service.php для запросов с фронта в .NET сайт

  2. перевести код 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 с указанием всех настроек виджета. Если все сделано правильно, то при запуске появится виджет с возможностью выбора пункта вывоза и расчета стоимости доставки:

Отображение пунктов вывоза
Отображение пунктов вывоза
Расчет стоимости доставки
Расчет стоимости доставки

© Habrahabr.ru