Русский
Русский
English
Статистика
Реклама

Json-rpc

Учим ASP.NET Core новым трюкам на примере Json Rpc 2.0

06.04.2021 04:10:16 | Автор: admin

Хотите добиться нестандартного поведения от aspnet core? Мне вот понадобилось добавить прозрачную поддержку Json Rpc. Расскажу о том, как я искал решения для всех хотелок, чтобы вышло красиво и удобно. Может быть, вам пригодятся знания о разных точках расширения фреймворка. Или о тонкостях поддержки Json Rpc, даже на другом стеке/языке.


В результате получилась библиотека, которая позволяет работать с Json Rpc, вообще не задумываясь, что он спрятан под капотом. При этом пользователю не нужно уметь ничего нового, только привычный aspnet mvc.


Введение


Хитрость в том, что в аспнете предусмотрены ручки для кастомизации вообще на любой случай. Если их найти и правильно применить, можно сделать почти без костылей всякие необычные решения. Порой эти ручки не самые удобные и не самые очевидные. Что-то уже устарело, какие-то вещи используются не часто и не на слуху, и вместе с этим не всегда есть подробная документация или гайды. Приходится барахтаться в статьях и исходниках.


В тексте под Aspnet подразумевается ASP.Net Core MVC, в частности все писалось на 2.2, с прицелом на то, что выйдет 5.x и допилим под него.

И Json Rpc протокол JSON RPC 2.0 поверх HTTP.

Еще для чтения стоит ознакомиться с терминами протокола: method, params, request, notification...


Зачем все это?


Я .NET техлид в банке Точка и работаю над инфраструктурой для шарповых сервисов. Стараюсь сделать так, чтобы разработчикам было удобно, а бизнесу быстро и без ошибок. Добрался до причесывания обменов под корпоративные стандарты, и тут началось...


У нас для синхронного общения по HTTP принят Json Rpc 2.0. А у шарпистов основной фреймворк ASP.NET Core MVC, и он заточен под REST. И на нем уже написано некоторое количество сервисов. Если немного абстрагироваться, то REST, JSON RPC, и любой RPC вообще об одном и том же: мы хотим, чтобы на удаленной стороне что-то произошло, передаем параметры, ожидаем результат. А еще транспорт совпадает, все по HTTP. Почему бы не воспользоваться привычным aspnet для работы с новым протоколом? Хочется при этом поддержать стандарт полностью: в компании много разных стеков, и у всех Json Rpc клиенты работают немного по-разному. Будет неприятно нарваться на ситуацию, когда запросы например от питонистов не заходят, и нужно что-то костылить.


Со стороны aspnet-а можно делать типовые вещи разными способами, лишь бы разработчику было удобно. Довольно много ресурсов командой уже потрачено на то, чтобы разобраться, какой из способов больше нам подходит. А ведь еще нужно поддерживать единообразие. Чтобы никто не сходил с ума, читая код сервиса, который написан коллегой полгода назад. То есть вы нарабатываете best practices, поддерживаете какие-то небольшие библиотечки вокруг этого, избавляетесь от бойлерплейта. Не хочется это терять.


Еще немаловажный момент: желательно, чтобы опыт у разработчиков не терял актуальность, то есть не затачивался на внутренние костыли и самописные фреймворки. Если ты три года пишешь веб-сервисы на шарпе, претендуешь на мидлосеньора, а потом не можешь сделать, например, авторизацию общепринятыми способами в пустом проекте, потому что у вас было принято писать в коде контроллера if(cookie.Contains(userName)) это беда.


Конечно, протокол уже реализован на C#, и не раз. Ищем готовые библиотеки. Выясняется, что они либо тащат свои концепции, то есть придется долго вникать, как это готовить. Либо делают почти то, что нужно, но тяжело кастомизируются и переизобретают то, что в aspnet уже есть.

Собираем хотелки и пишем код


Чего хочется добиться? Чтобы как обычно писать контроллеры, накидывать фильтры и мидлвари, разбирать запрос на параметры. Чтобы наработанные best practices и библиотеки для aspnet подходили as-is. И при этом не мешать работать существующему MVC коду. Так давайте научимся обрабатывать Json Rpc теми средствами, что нам предоставляет фреймворк!


Request Routing


Казалось бы, HTTP уже есть, и нам от него надо только обрабатывать POST и возвращать всегда 200 OK. Контент всегда в JSON. Все прекрасно, сейчас напишем middleware и заживем.
Но не тут-то было! Мидлварь написать можно, только потом будем получать все запросы в один action, а в нем придется switch(request.Method) или запускать хендлеры из DI каким-нибудь костылем. А кто будет авторизацию и прочие фильтры прогонять в зависимости от метода? Переизобретать все это заново ящик Пандоры: делаешь свой аналог пайплайна, а потом придется поддерживать общий код и для aspnet, и для своего пайплайна. Ну, чтобы не было внезапных различий между тем, как вы проверяете роли или наличие какого-то заголовка.
Значит, придется влезть в роутинг и заставить его выбирать controller и action, глядя на тело HTTP запроса, а не только на url.


ActionMethodSelectorAttribute


К сожалению, все часто используемые инструменты для роутинга не позволяют парсить запрос или выполнять произвольный код. Есть стандартный роутер, но его нельзя без лишних проблем расширить или что-то в нем перегрузить. IRouter целиком писать, конечно же, не надо, если мы хотим гарантировать что обычный MVC не сломается. Казалось бы, есть Endpoint Routing, но в 2.2 сделана его начальная и неполная реализация, и чего-то полезного с ним напрямую не сделаешь. Костыли типа переписывания еndpoint на свой (после того, как Endpoint Routing отработал) почему-то не взлетели. Можно, конечно, прямо в middleware сделать редирект на нужный url, и это будет работать, только url испортится.


После долгих поисков был найден ActionMethodSelectorAttribute, который делает как раз то, что нужно: позволяет вернуть true/false в момент выбора controller и action! У него есть контекст с именем текущего метода-кандидата и его контроллера. Очень удобно.


Остается две проблемы: как повесить атрибут на все нужные нам контроллеры, и как не парсить тело каждый раз, когда выполняется проверка на "пригодность" метода?


Conventions


Атрибуты на контроллеры можно расставлять кодогенерацией, но это слишком сложно. У фреймворка и на этот случай есть решение: IControllerModelConvention, IActionModelConvention. Это что-то вроде знакомых многим Startup Filters: запускаются один раз на старте приложения и позволяют сделать все что угодно с метаданными контроллеров и методов, например переопределить роутинг, атрибуты, фильтры.


С помощью conventions мы можем решить сразу несколько задач. Сначала определимся, как мы будем отличать Json Rpc контроллеры от обычных. Не долго думая, идем по тому же пути, что и Microsoft: сделаем базовый класс по аналогии с ControllerBase.


public abstract class JsonRpcController : ControllerBase {}

То есть наши контроллеры с точки зрения пользователя ничем не будут отличаться от обычных. Только наследовать придется немного иначе. Теперь в conventions можно легко понять, стоит ли вмешиваться в метаданные, или нет. Вот так будет выглядеть


полезный код в ControllerConvention
public void Apply(ControllerModel controllerModel){    if (!typeof(JsonRpcController).IsAssignableFrom(controllerModel.ControllerType))    {        return;    }    controllerModel.Selectors.Clear();    controllerModel.Filters.Insert(0, new ServiceFilterAttribute(typeof(JsonRpcFilter)));}

Selectors отвечают за роутинг, и я честно не смог найти, почему это коллекция. В любом случае, нам не нужен стандартный роутинг по правилам MVC, поэтому удаляем все, что есть. Забегая вперед, применяем JsonRpcFilter, который будет отвечать за оборачивание ActionResult.


А вот ActionConvention
public void Apply(ActionModel actionModel){    if (!typeof(JsonRpcController).IsAssignableFrom(actionModel.Controller.ControllerType))    {        return;    }    actionModel.Selectors.Clear();    actionModel.Selectors.Add(new SelectorModel()    {        AttributeRouteModel = new AttributeRouteModel() {Template = "/api/jsonrpc"},        ActionConstraints = {new JsonRpcAttribute()}    });}

Здесь на каждый метод в наших контроллерах мы повесим один и тот же route, который потом вынесем в настройки.


И добавим тот самый атрибут
class JsonRpcAttribute : ActionMethodSelectorAttribute{    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)    {        var request = GetRequest();  // пока не понятно как        // return true если action подходит под запрос, например:        return request.Method == action.DisplayName;    }}

Осталось решить, как распарсить тело запроса только один раз, чтобы проверять его поля в этом атрибуте. Ну и на самом деле все чуть сложнее из-за того, как мы хотим интерпретировать поле Method, об этом позже.


Middleware


Пишем middleware, которая будет проверять, что запрос похож на Json Rpc: правильный Content-Type, обязательно POST, и тело содержит подходящий JSON. Запрос можно десериализовать в объект и сложить в HttpContext.Items. После этого его можно будет достать в любой момент.


Есть одна загвоздка: у middleware еще нет информации о типе, в который нужно десериализовать params, поэтому мы пока оставим их в виде JToken.


Подключать middleware будем с помощью IStartupFilter, чтобы не напрягать пользователя. Вообще, решение спорное, но фильтр всегда можно убрать, если очень нужен определенный порядок middleware.


Parameter Binding


Мы научились подбирать контроллер и метод под запрос. Теперь нужно что-то делать с аргументами. Можно достать то, что десериализовала middleware из HttpContext.Items, и десериализовать JToken вручную в нужный тип, но это бойлерплейт и ухудшение читаемости методов. Можно взять JSON целиком из тела запроса с помощью [FromBody], но тогда всегда будет присутствовать шапка протокола: id, версия, метод. Придется каждую модель оборачивать этой шапкой: Request<MyModel> или class MyModel: RequestBase, и снова получим бойлерплейт.


Эти решения были бы еще терпимы, если бы протокол не вставлял палок в колеса.


Разные params


Json Rpc считает, что параметры, переданные массивом [] это одно и то же, что и параметры, переданные объектом {}! То есть, если нам прислали массив, нужно подставлять их в свой метод по порядку. А если прислали объект, то разбирать их по именам. Но вообще, оба сценария должны работать для одного и того же метода. Например, вот такие params эквивалентны и должны одинаково биндиться:


{"flag": true, "data": "value", "user_id": 1}[1, "value", true]

public void DoSomething(int userId, string data, bool flag)

Раз мы замахнулись на полную поддержку протокола, нужно что-то придумывать. Желательно не утруждая пользователей этими тонкостями.


Реализация


Посмотрим, что нам доступно для управления биндингом. Есть IModelBinder и IModelBinderProvider, но они смотрят на тип объекта. Заранее мы не знаем, какой тип пользователь захочет биндить. Может быть, int или DateTime. Мы не хотим конфликтовать с aspnet, поэтому просто добавить свой биндер для всех типов нельзя. Есть IValueProvider, но он возвращает только строки. Наконец, есть атрибуты FromBody, FromQuery и так далее. Смотрим в реализацию, находим интерфейс IBinderTypeProviderMetadata. Он нужен, чтобы возвращать нужный binder для параметра. Как раз то, что нужно!


Пишем свой FromParamsAttribute
 [AttributeUsage(AttributeTargets.Parameter)]public class FromParamsAttribute : Attribute, IBindingSourceMetadata, IBinderTypeProviderMetadata{    public BindingSource BindingSource => BindingSource.Custom;    public Type BinderType => typeof(JsonRpcModelBinder);}

Теперь атрибут придется повесить на каждый параметр. Вернемся к conventions и напишем реализацию IParameterModelConvention. Только оказывается, что нужно не использовать атрибут, а создавать


`BindingInfo` с той же информацией
public void Apply(ParameterModel parameterModel){    if (!typeof(JsonRpcController).IsAssignableFrom(parameterModel.Action.Controller.ControllerType))    {        return;    }    if (parameterModel.BindingInfo == null)    {        parameterModel.BindingInfo = new BindingInfo()        {            BinderType = typeof(JsonRpcModelBinder),            BindingSource = BindingSource.Custom        };    }}

Проверка на BindingInfo == null позволяет использовать другие атрибуты, если нужно. То есть можно смешивать FromParams и штатные FromQuery, FromServices. Ну а по умолчанию, если ничего не указано, convention применит BindingInfo, аналогичный FromParams.


Удобства


Стоит учесть сценарий, когда неудобно разбирать params на отдельные аргументы. Что, если клиент просто прислал свой объект "как есть", а в нем очень много полей? Нужно уметь биндить params целиком в один объект:


{"flag": true, "data": "value", "user_id": 1}

public void DoSomething(MyModel model)

Но что делать, если придет json-массив? Теоретически, можно бы узнать порядок properties в объекте, и биндить по порядку. Но из рефлексии этого не достать, такая информация просто не сохраняется. Поэтому массив в объект сбиндить не получится без костылей типа атрибутов с номерами Но можно сделать проще: сделаем эту фичу опциональной. Да, она не работает с массивами, что ломает поддержку протокола, поэтому придется выбирать. Добавим параметр в наш атрибут:


BindingStyle
public enum BindingStyle { Default, Object, Array }...public FromParamsAttribute(BindingStyle bindingStyle){    BindingStyle = bindingStyle;}

Default поведение по умолчанию, когда содержимое params биндится в аргументы. Object когда пришел json-объект, и мы биндим его в один параметр целиком. Array когда пришел json-массив и мы биндим его в коллекцию. Например:


// это успешно сбиндится: {"flag": true, "data": "value", "user_id": 1}// а это будет ошибкой: [1, "value", true]public void DoSomething1([FromParams(BindingStyle.Object)] MyModel model)// это успешно сбиндится: [1, "value", true]// а это будет ошибкой: {"flag": true, "data": "value", "user_id": 1}public void DoSomething2([FromParams(BindingStyle.Array)] List<object> data)

Всю эту логику придется реализовать в JsonRpcModelBinder. Приводить здесь код нет смысла: его много, но он тривиальный. Разве что упомяну несколько интересных моментов:


Как сопоставить имя аргумента и ключ в json-объекте?


JsonSerizlizer не позволяет "в лоб" десериализовать ключ json объекта как шарповое имя property или аргумента. Зато позволяет сериализовать имя в ключ.


// вот так не получится{"user_id": 1} => int userId//  зато можно наоборот и запомнить это в метаданныхint userId => "user_id"

То есть нужно для каждого аргумента узнать его "json-имя" и сохранить в метаданных. У нас уже есть conventions, там и допишем нужный код.


Учимся у aspnet


Кода в биндере получается много, как ни крути. Оказалось полезно подсмотреть в реализацию стандартного ModelBinder и взять оттуда подходы: свой контекст, обработка ошибок, результат биндинга как отдельный объект.


Регистрация в DI-контейнере


Биндер не совсем нормальный сервис. Предполагается, что мы должны регистрировать его через binder provider, но тогда он будет конфликтовать с существующими. Поэтому придется


резолвить зависимости вручную
public Task BindModelAsync(ModelBindingContext context){    var service = context.HttpContext.RequestServices.GetServices<IService>();    // ...}

Error handling


Все ошибки протокол предлагает возвращать в виде специального ответа, в котором могут быть любые детали. Еще там описаны некоторые крайние случаи и ошибки для них. Придется перехватывать все exception-ы, заворачивать их в Json Rpc ответ, уметь прятать stack trace в зависимости от настроек (мы же не хотим высыпать все подробности на проде?). А еще нужно дать пользователю возможность вернуть свою Json Rpc ошибку, вдруг у кого-то на этом логика построена. В общем, ошибки придется перехватывать на разных уровнях. После написания десятого try/catch внутри catch поневоле начинаешь задумываться, что неплохо бы иметь возможность писать код с гарантией отсутствия exception-ов, или хотя бы с проверкой, что ты перехватил все, что можно...


Action вернул плохой ActionResult или Json Rpc ошибку


Возьмем IActionFilter.OnResultExecuting и будем проверять, что вернулось из метода: нормальный объект завернем в Json Rpc ответ, плохой ответ, например 404, завернем в Json Rpc ошибку. Ну или метод уже вернул ошибку по протоколу.


Binding failed


Нам пригодится IAlwaysRunResultFilter.OnActionExecuting: можно проверить context.ModelState.IsValid и понять, что биндинг упал. В таком случае вернем ошибку с сообщением, что не получилось у биндера. Если ничего не делать, то в action попадут кривые данные, и придется проверять каждый параметр на null или default.


Схожим образом работает стандартный ApiControllerAttribute: он возвращает 400, если биндинг не справился.


Что-то сломалось в pipeline


Если action или что-нибудь в pipeline выбросит exception, или решит записать HttpResponse, то единственное место, где мы еще можем что-то сделать с ответом, это middleware. Придется и там проверять, что получилось после обработки запроса: если HTTP статус не 200 или тело не подходит под протокол, придется заворачивать это в ошибку. Кстати, если писать ответ прямо в HttpResponse.Body, то сделать с ним уже ничего не получится, но для этого тоже будет решение чуть ниже.


Ошибки это сложно


Для удобства пригодится класс, который будет отвечать за проверку требований протокола (нельзя использовать зарезервированные коды), позволит использовать ошибки, описанные в протоколе, маскировать exception-ы, и подобные штуки.


class JsonRpcErrorFactory{    IError NotFound(object errorData){...}    IError InvalidRequest(object errorData){...}    IError Error(int code, string message, object errorData){...}    IError Exception(Exception e){...}    // и так далее}

Batch


Batch-запросы aspnet не поддерживает никак. А они требуются стандартом. И хочется, чтобы на каждый запрос из батча был свой пайплайн, чтобы в тех же фильтрах не городить огород. Можно, конечно, сделать прокси, который будет разбирать батч на отдельные запросы, отправлять их на localhost, потом собирать ответ. Но это кажется безумным оверхедом из-за сериализации HTTP body в байты, установления соединения После долгих путешествий по issues в Github, находим грустный тред о том, что батчи когда-то были, но пока нет и неизвестно когда вернутся. А еще они есть в OData, но это целый отдельный мир, фреймворк поверх фреймворка погодите-ка, мы же тоже пишем что-то такое!. Там же находим идею и репозиторий с реализацией: можно скопировать HttpContext и в middleware позвать next() со своим контекстом, а потом собрать результат и отправить все вместе уже в настоящий HttpContext. Это поначалу немного ломает мозг, потому что мы привыкли к мантре: нужно передавать управление вызовом next(context), и по-другому никто эту штуку не использует.


Таким образом, middleware будет парсить Json Rpc запрос, создавать копию контекста, и вызывать пайплайн дальше. Это же пригодится для перехвата ошибок, если кто-то решит писать прямо в HttpResponse.Body: мы вместо настоящего body подсунем MemoryStream и проверим, что там валидный JSON.


У этого подхода есть минус: мы ломаем стриминг для больших запросов/ответов. Но что поделать, JSON не подразумевает потоковую обработку. Для этого, конечно, есть разные решения, но они гораздо менее удобны, чем Json.NET.


ID


Протокол требует поле id в запросе, при чем там могут быть число, строка или null. В ответе должен содержаться такой же id. Чтобы одно и то же поле десериализовалось как число или строка, пришлось написать классы-обертки, интерфейс IRpcId и JsonConverter, который проверяет тип поля и десериализует в соответствующий класс. В момент, когда мы сериализуем ответ, из HttpContext.Items достаем IRpcId и прописываем его JToken-значение. Таким образом, пользователю не надо самому заморачиваться с проставлением id и нет возможности забыть об этом. А если нужно значение id, можно достать из контекста.


Notification


Если id отсутствует, то это не запрос, а уведомление (notification). На notification не должен уходить ответ: ни успешный, ни с ошибкой, вообще никакой. Ну, то есть по HTTP-то мы вернем 200, но без тела. Чтобы все работало одинаково для запросов и нотификаций, пришлось выделить абстракцию над ними, и в некоторых местах проверять, запрос ли это и нужно ли сериализовать ответ.


Сериализация


Aspnet умеет сериализовать JSON. Только у него свои настройки, а у нас свои. Сериализация настраивается с помощью Formatters, но они смотрят только на Content-Type. У Json Rpc он совпадает с обычным JSON, поэтому просто так добавить свой форматтер нельзя. Вложенные форматтеры или своя реализация плохая идея из-за сложности.


Решение оказалось простым: мы уже оборачиваем ActionResult в фильтре, там же можно


подставить нужный форматтер
...var result = new ObjectResult(response){    StatusCode = 200,};result.Formatters.Add(JsonRpcFormatter);result.ContentTypes.Add(JsonRpcConstants.ContentType);...

Здесь JsonRpcFormatter это наследник JsonOutputFormatter, которому переданы нужные настройки.


Configuration


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


Имя метода


У Json Rpc запросов есть поле method, которым определяется, что должно быть вызвано на сервере. И это поле просто строка. Ей пользуются как угодно. Придется научить сервер понимать распространенные варианты.


public enum MethodStyle {ControllerAndAction, ActionOnly}

ControllerAndAction будет интерпретировать method как class_name.method_name.


ActionOnly просто method_name.


Кстати, возможны коллизии, например когда у разных контроллеров есть одинаковые методы. Проверять такие ошибки удобно в conventions.


Сериализация


Еще встает вопрос с JSON-сериализацией. Формат "шапки" строго обозначен в протоколе, то есть переопределять его нужно примерно никогда. А вот формат полей params, result и error.data оставлен свободным. Пользователь может захотеть сериализацию со своими особыми настройками. Нужно дать такую возможность, при этом не позволяя сломать сериализацию шапки и не накладывая особых требований на пользователя. Например, для работы с шапкой используются хитрые JsonConverterы, и не хотелось бы чтобы они как-то торчали наружу. Для этого сделана минимальная обертка поверх JsonSeralizer, чтобы пользователь мог зарегистрировать в DI свой вариант и не сломать REST/MVC.


Нестандартные ответы


Бывает, что нужно вернуть бинарный файл по HTTP, или Redirect для авторизации. Это явно идет в разрез с Json Rpc, но очень удобно. Такое поведение нужно разрешать при необходимости.


Объединяем все вместе


Добавим классы с опциями, чтобы рулить умолчаниями
public class JsonRpcOptions{    public bool AllowRawResponses { get; set; }  // разрешить ответы не по протоколу?    public bool DetailedResponseExceptions { get; set; }  // маскировать StackTrace у ошибок?    public JsonRpcMethodOptions DefaultMethodOptions { get; set; }  // см. ниже    public BatchHandling BatchHandling { get; set; }  // задел на параллельную обработку батчей в будущем}public class JsonRpcMethodOptions{        public Type RequestSerializer { get; set; }  // пользовательский сериалайзер    public PathString Route { get; set; }  // маршрут по умолчанию, например /api/jsonrpc    public MethodStyle MethodStyle { get; set; }  // см. выше}

И атрибуты, чтобы умолчания переопределять:


  • FromParams про который было выше
  • JsonRpcMethodStyle чтобы переопределить MethodStyle
  • JsonRpcSerializerAttribute чтобы использовать другой сериалайзер.

Для роутинга свой атрибут не нужен, все будет работать со стандартным [Route].


Подключаем


Пример кода, который использует разные фичи. Важно заметить, что это никак не мешает обычному коду на aspnet!


Startup.cs
services.AddMvc()    .AddJsonRpcServer()    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);// или с опциямиservices.AddMvc()    .AddJsonRpcServer(options =>    {        options.DefaultMethodOptions.Route = "/rpc";        options.AllowRawResponses = true;    })    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Контроллер
public class MyController : JsonRpcController{    public ObjectResult Foo(object value, bool flag)    {        return Ok(flag ? value : null);    }    public void BindObject([FromParams(BindingStyle.Object)] MyModel model)    {    }    [Route("/test")]    public string Test()    {        return "test";    }    [JsonRpcMethodStyle(MethodStyle.ActionOnly)]    public void SpecialAction()    {    }    [JsonRpcSerializer(typeof(CamelCaseJsonRpcSerializer))]    public void CamelCaseAction(int myParam)    {    }}

Клиент


Конечно, для полного удобства нужен клиент. Пришлось разбить библиотеку на три сборки: сервер, клиент и общий код (модели и json-конвертеры).


HttpClient


В .net core HttpClient научили работать с DI, типизировать и вообще все стало гораздо удобнее. Грех не воспользоваться!


Batch


По протоколу ответы на батч-запросы нужно разбирать по их id. Поэтому, чтобы сделать батч-запрос, пользователь должен заранее задать id, и запомнить их. А потом перебирать их, и из батч-ответа пытаться достать конкретный ответ или ошибку.


Обработка ошибок


Снова сложности с обработкой ошибок. Дело в том, что мы не знаем, с какими правилами сериализации сервер вернул ошибку: со стандартными, как в "шапке", или с кастомными, например когда мы договорились на клиенте и на сервере использовать camelCase, но у сервера что-то сломалось в middleware и дело до нашего action не дошло вообще. Поэтому придется пробовать десериализовать и так, и так. Здесь нет очевидно хорошего решения, поэтому интерфейс response содержит


Разные методы для интерпретации ответа
T GetResponseOrThrow<T>();  // достать успешный ответ, если нет - достать ошибку и выбросить ее как исключениеT AsResponse<T>(); // только достать ответError<JToken> AsAnyError(); // достать ошибку, не десериализуя ееError<T> AsTypedError<T>(); // достать ошибку по правилам сериализации как в запросе или по дефолтным, если не получилосьError<ExceptionInfo> AsErrorWithExceptionInfo(); // достать ошибку с деталями exception-а с сервера

Для удобства в самом простом случае есть GetResponseOrThrow() или ожидаемый ответ, или исключение. Для детального разбора все остальные методы.


Developer Experience


Я считаю, что получилось решение, когда разработчик может подключить и забыть, что там какой-то Json Rpc. При этом можно полагаться на свой опыт работы с aspnet, использовать привычные подходы и без проблем подключать любые сторонние библиотеки к приложению. С другой стороны, есть возможность переопределять какое-то поведение, мало ли какие потребности возникнут. Часто используемые штуки вынесены в параметры и атрибуты, а если не хватает, можно посмотреть код и подменить что-то: все методы виртуальные, сервисы используются по интерфейсам. Можно расширить или написать полностью свою реализацию.


TODO


В планах: добавить поддержку aspnetcore 5.x, добить покрытие тестами до 100%, перевести документацию на русский и добавить параллельную обработку батчей. Ну и, конечно же, поддерживать проект как нормальный open source: любой фидбек, пуллреквесты и issues приветствуются!


Сразу отвечу на вопросы про перформанс и production: уже год используется в проде на не очень нагруженных проектах, поэтому пока что в производительность не упирались и даже не замеряли, и не сравнивали с альтернативами. В коде есть очевидные места, которые можно пооптимизировать, чтобы лишний раз не гонять JSON-сериализацию, например.


Ссылки


Исходники


Документация


Бонус


Статья лежала в черновиках почти год, за это время к библиотеке была добавлена поддержка автодокументации Swagger и OpenRpc. А еще сейчас в разработке поддержка OpenTelemetry. Кому-нибудь интересны подробности? Там местами такая жуть...

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru