Генерация PDF-отчетов с помощью ASP.Net и React

Не могу не упомянуть про один очень интересный доклад (и open-source шаблон!) с нашего 6-го .NET-митапа.

Коллега Семён Конончук рассказывал про очень часто встречающуюся задачу — генерацию отчетов.

И если отчеты для «внутреннего пользования»/мониторинга вполне можно генерировать какими-нибудь Графанами или другими html-инструментами, то к отчетам для пользователей совсем другие требования.

Они должны быть красивыми, для них создается специальный дизайн, и mrtg-like набор графиков (см ниже :)) вряд ли кого-то устроит

MRTG graph

Для решения задачи создания таких богатых PDF-отчетов (которые можно распечатать, а можно и просто в виде файлика куда-нибудь положить), а также переиспользования в отчетах кода из существующего фронтэнда и был создан шаблон проекта PDFGenerator. Если кратко, он возвращает PDF при вызове метода ASP.Net контроллера по http. Само содержимое отчета — на Реакте, данные для отчета собирает произвольным способом тот же ASP.Net сервис.

А вот и картинка для привлечения внимания, а для детального описания как оно работает — идите сразу на гитхаб.

img

Выложенный проект — это именно пример/шаблон. Если хотите использовать у себя — скачивайте, меняйте namespace на свои, запускайте — и радуйтесь :)

А если хотите узнать как мы к этому пришли и какие альтернативы рассматривали — вот вам и видос доклада. Приятного просмотра!

TomskDotNet #6 — первый пост-коронавирусный митап и выход .NET 5!

Наконец-то! После почти года перерыва митапы потихоньку возвращаются!

22 октября мы провели очередной — и очень неплохой — митап (фото и видео — традиционно на сайте). Лично я был очень удивлен такому большому количеству смельчаков, не побоявшихся прийти к нам в оффлайн. Следуя коронавирусным традициям, в этот раз у нас была и онлайн часть, и даже одному из «удаленных» участников достался сертификат от Jetbrains — наш приятный подарок за активность и хорошие вопросы.

У меня на этом митапе был небольшой вступительный доклад про грядущий выход .NET 5, а сейчас, спустя месяц, я могу поделиться и собственным опытом переезда на новую версию с 3.1. Мои небольшие пет-проджекты и чуть большая open-source библиотека авторизации переехали за пару вечеров без каких-либо проблем. По сравнению с миграцией 2.2 -> 3.0 (которая, пожалуй, заняла недели 2-3) — это просто сказка. Единственный breaking-change с которым мне пришлось столкнуться — это небольшое изменение в контексте авторизации. В остальном обновление свелось к чистке Startup.cs и изменению версий зависимостей в .csproj. Идеально! :)

Так что если задумываетесь над обновлением — не задумывайтесь :) В сети куча статей про 20% рост производительности, так что причин откладывать нет ну вообще никаких (ну, разве что помните, что .NET 5 — не LTS и обновляться до .NET 6 придется оперативно после его выхода).

Ну и смотрите видеозапись доклада, если интересно :)

Реализация HTTP Patch в ASP.Net Core 3

Классический набор операций над сущностями, который содержит любая система — это CRUD — Create, Read, Update, Delete. Реализация трех из них (Создания, Чтения и Удаления) в ASP.Net Core не вызывает проблем и контроллер REST API (с HTTP методами POST GET и DELETE) для этих операций можно легко и просто создать из встроенного шаблона Visual Studio. Четвертую операцию — обновление — шаблон тоже создает автоматически. Но есть одна тонкость.

По умолчанию для обновления шаблон использует операцию HTTP PUT. Давайте посмотрим на контроллер целиком, и на операцию PUT в отдельности, чтобы разобрать, что же с ней не так.


// PUT: api/Users/5
[HttpPut("{id}")]
public async Task<IActionResult> PutUser(int id, User user)
{
	_context.Entry(user).State = EntityState.Modified;

	await _context.SaveChangesAsync();
	
	return NoContent();
}

(автосгенерированный VisualStudio метод обновления сущности. Обработка ошибок убрана, как не относящаяся к сути вопроса)

К самому HTTP PUT нет никаких претензий, но и по определению и по коду видно, что PUT операция меняет объект целиком. Однако очень часто более полезной оказывается частичное изменение объекта (например, возможность изменить только имя у сущности User). Для этого как раз предназначен HTTP метод PATCH, и сам запрос в этом случае выглядит примерно так:

PATCH https://localhost:5001/api/Users/1
{
  "name": "Artur",
}

В этом и последующих примерах предположим, что сущность User выглядит как-то так:

public class User
{
	public int Id { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }

	public User Mother { get; set; }
	public int? MotherId { get; set; }

	public User Father { get; set; }
	public int? FatherId { get; set; }
}

В теории все хорошо, а как же это будет реализовываться на практике с ASP.Net Core? Сигнатура метода, на первый взгляд, измениться не должна — мы по прежнему принимаем id пользователя и объект:

[HttpPatch("{id}")]
public async Task<IActionResult> PatchUser(int id, PatchUserDto patchUserDto)

А как же будет выглядеть тело метода? Какие значения из пришедшей DTO надо записать в сущность User‘a? Для не-nullable полей (например, int) можно сделать поля в PatchDto — nullable и обновлять только те значения, которые не null (например, поле int Age в DTO можно превратить в int? Age). Но у нас есть и nullable поля — string Name или int? ParentId. Для них непонятно, как определить — было ли это поле передано в DTO или нет. Очень показательный пример запроса:

PATCH https://localhost:5001/api/Users/1
{
  "fatherId": null,
}

fatherId передан в запросе, и значение User.FatherId надо обнулить, а, допустим, MotherId в запросе не передавался и, соответственно, обнулять его не надо. Однако в patchUserDto значения этих двух полей будут идентичными — null.

Как же решать эту типичную проблему? Как ни странно, публичных обсуждений в интернете не слишком много. Есть раз, два, три вопроса на stackoverflow (почитайте их, как минимум чтобы детальнее понимать, какую проблему мы решаем). Из предлагаемых решений:

  1. Вариация на тему nullable-полей: предлагается поля сделать тип Settable<T>, который имеет свойство IsSet == true, если поле присутствовало в http-запросе и false, если поля в запросе не было (и, соответственно, его обнулять у нашей сущности не надо).
    Выглядеть это будет как-то так:
public class SettablePatchUserDto
{
	public string Name { get; set; }

	public Settable<int> MotherId { get; set; }

	public Settable<int> FatherId { get; set; }
}

Решение неплохое, но есть и некоторые минусы:,

  • неприменим для строк
  • нетипичная работа с nullable значениями (к примеру, передать null в поле MotherId можно, но определять это нужно по MotherId.HasValue.
  • внедрение нового незнакомого типа (Settable<T>).

    2. Ответы на второй и третий вопросы в чем-то схожи. Они предлагают расширить PatchDto свойством HashSet<string> PropertiesInHttpRequest, которое будет содержать список свойст, которые были переданы в http-запросе. В этом случае можно будет легко и однозначно определить, нужно ли изменять свойство сущности в БД (в том числе в случае null значений).

Второй путь и мне показался более перспективным, и именно им мы и воспользовались. Любопытствующие могут сразу заглянуть в исходники на гитхаб, где выложен пример проекта с использованием PATCH метода.

Наиболее важными участками кода я поделюсь и здесь. Собственно, PATCH метод:

[HttpPatch("{id}")]
public async Task<IActionResult> PatchUser(int id, PatchUserDto patchUserDto)
{
	var user = await _context.Users.SingleAsync(x => x.Id == id);
	
	// could be as well automated with smth like Automapper if you'd like to
	user.Age = patchUserDto.IsFieldPresent(nameof(user.Age)) ? patchUserDto.Age : user.Age;
	user.Name = patchUserDto.IsFieldPresent(nameof(user.Name)) ? patchUserDto.Name : user.Name;
	user.FatherId = patchUserDto.IsFieldPresent(nameof(user.FatherId)) ? patchUserDto.FatherId : user.FatherId;
	user.MotherId = patchUserDto.IsFieldPresent(nameof(user.MotherId)) ? patchUserDto.MotherId : user.MotherId;
	
	await _context.SaveChangesAsync();
	
	return NoContent();
}

Чтобы этого добиться, нужно кастомизировать ContractResolver в Startup’e таким образом:

services
	.AddControllers()
	.AddNewtonsoftJson(options =>
	{
		options.SerializerSettings.ContractResolver = new PatchRequestContractResolver();
	});

Этот способ показал себя как понятный, очевидный и минимально интрузивный.

Мы его внедрили и это успешно работает в наших проектах. Я так же поделился этим способом на stackoverflow, прошу любить и лайкать :)

NSwag vs Swashbuckle for Swagger, Typescript client API generation, and fighting undefined/nulls in DTO

I have already expressed my love with Swagger :) Over time, however, I met Swagger’s sister — NSwag — and fell in love with her even more :)

Long story short, NSwag doesn’t have an IFormFile issues I was solving in Swagger out of the box. But the reason I moved is actually a bit different. We wanted to use OpenAPI definitions for autogenerating clients for our API. Writing something like this:

export interface IFirmwareInfoDto {
    exists: boolean;
    versionString: string;
    releaseDate: string;
    deviceGeneration: DeviceGeneration;
}

async getAvailableFiles(options?: AxiosRequestConfig): Promise<IFirmwareInfoDto[]> {
    const response = await axios.get<IFirmwareInfoDto[]>(
        `/api/firmwareDownload/all`,
        {...defaultOpts, ...options},
    );
    return response.data;
},

by hand for every API action is not only tedious, but also error prone. Thank goodness, there are tools for automatic generation of those based on OpenAPI definitions. We used OpenAPI Generator initially (since Swashbuckle doesn’t have anything built-in), and it was good, but, as usual, devil was in the details. And the devil here was C# enum handling.


Actually, OpenAPI Generator had no issues with handling enum. It just doesn’t handle them at all :) There were just two options, both of which were affecting API itself: we could either express enum elements as string or as number. That was suboptimal. So, having a C# DTO and Action like this:

public class UserDto
{
	public string Name { get; set; }
	public DateTime BirthDate { get; set; }
	public Sex Sex { get; set; }
}

[HttpGet]
public UserDto Get(int id)
{
	return new UserDto()
	{
		Name = "Artur",
		BirthDate = new DateTime(1985, 5, 12),
		Sex = Sex.Male,
	};
}

We got the following Dto generated in Typescript

export interface UserDto {
    name?: string | null;
    birthDate?: Date;
    sex?: Sex;
}

export enum Sex {
    NUMBER_0 = 0,
    NUMBER_1 = 1
} 

We could get Sex enum generated like this:

export enum Sex {
    Male = 'Male',
    Female = 'Female'
} 

But only if we configure our API to behave the same way (i.e. you would receive the following JSON as a result of an http call

{
  'name': 'Artur',
  'birthDate': '2009-02-15T00:00:00Z',
  'sex': 'Male'
}

It’s a debatable topic, whether to use strings or ints for enums in the API, but in some cases (like Flags enums, where several values could be combined) ints are unavoidable.

So, we tried NSwag and thankfully, here’s what we have with it:

export enum Sex {
    Male = 0,
    Female = 1,
}

The key difference, is that NSwag has it’s own client generator, and it takes into account some of extensions that NSwag adds when generating API definition (i.e. string representation of enum values are stored in description fields).

Of course, NSwag also generates nice little (well, not so little :)) typescript client as well:

export class UserClient {
    constructor(baseUrl?: string, instance?: AxiosInstance) {
      // ...
    }

    get(id: number): Promise<UserDto> {
      // ...
    }
}

…and we have lived happily ever after with NSwag, unless we discovered another glitch. It’s called undefined. If we take a closer look at UserDto we could notice, that all properties are undefinable (this little ? sign next to the property name):

export interface IUserDto {
    name?: string | undefined;
    birthDate?: Date;
    sex?: Sex;
}

It’s not only inconvenient (because in typescript you would always have to check, if the value is really defined or not), but it’s also just wrong, because with aforementioned c# definition both birthDate and sex will always be defined.

Luckily, this could be fixed rather easy. We have introduced a special RequireValueTypesSchemaProcessor, that you could integrate into NSwag with oneliner:

options.SchemaProcessors.Add(new RequireValueTypesSchemaProcessor());

and DTOs will be automatically fixed for you:

export interface IUserDto {
    name: string;
    birthDate: Date;
    sex: Sex;
}

Go take a look at the sources if you want more details! You will also receive a bonus — the way to easily get back to generating undefinable for some DTOs where you particularly want that (e.g. for HTTP PATCH requests) :)

As usual, you could get complete C# project along with all generated client examples from github. Check out clients folder for already generated clients, or run yarn nswag-client or yarn swashbuckle-nswag-client to regenerate them.

Real-time синхронизация данных между ASP.NET и React

В сентябре прошлого года я выступал на крупнейшей томской конференции — «Городе АйТи» с докладом про синхронизацию данных.

Мы в МЦЦ Томск сейчас ведем разработку CRM системы, в которой такая синхронизация нашла очень удачное применение. Как я и рассказывал в докладе, мы синхронизируем «словари» — редко меняющиеся значения списковых элементов, а также пользователей и роли — потому что обращение к ним требуется почти на каждом экране.

Конечно, в процессе внедрения столкнулись с некоторыми особенностями, о которых в докладе сказано не было:

  1. Мы синхронизируем DTO, но, например, UserDto зависит не только от сущности User БД. Например, она также содержит информацию о ролях. Соответственно, пришлось синхронизировать обновления User’ов при обновлении связанных сущностей.
  2. При инициализации синхронизации на стороне React необходимо следить за последовательностью действий. Мы делаем так:
    1. Стартуем SignalR-соединение и собираем все пришедшие изменения, но не применяем их.
    2. Запрашиваем текущие значения всех сущностей с Backend (GET /users).
    3. После получения списка сущностей применяем все полученные изменения
    4. Продолжаем синхронизацию в обычном режиме (применяем изменения как только они приходят
  3. Процедуру инициализации соединения повторяем при разрыве SignalR-подключения
  4. Не все изменения в сущностях можно получить до вызова SaveChanges(). Например, идентификаторы новых сущностей (для которых будет выполнена команда INSERT) недоступны до SaveChanges(). Таким образом необходимо производить дополнительную обработку списка изменений после SaveChanges.

Если интересны детали или обновленные примеры кода — пишите, обсудим!

А также можете скачать презентацию как приятный бонус :)

OAuth в SPA или неожиданные сложности интеграции логина через соцсети в React с Asp.Net Core

Это история про то, как казалось бы типичная задача интеграции входа через соц.сети в React/Asp.Net Core приложении может превратиться в длинную сагу и закончиться open-source библиотекой :)

Если читать лень, то можно сразу пойти на гитхаб, где и посмотреть весёлую гифку и прочую документацию по интеграции и использованию, ну а здесь я расскажу чуть подробнее :)


В Asp.Net Core существует замечательная встроенная интеграция с внешними провайдерами аутентификации (OAuth/OpenId и прочее нестандартное), а также сторонние плагины, поддерживающие аутентификацию даже через VK. Однако весь этот механизм подразумевает, что у вас обычное server-side приложение (с forms-авторизацией), и никаких собственных access_token’ов, которые привычны в SPA вам генерироваться не будет.

Вот вот мне и загорелось желание воспользоваться всем этим огромным количеством готовых решений и подружить его с SPA. Как схема работы должна выглядеть в идеале? Согласно AuthCode Flow (который считается рекомендуемым для использования в SPA), это должно выглядеть примерно так:

SPA получает AuthCode у стороннего провайдера и передает его на бэкэнд приложения. Бэкэнд проверяет верность кода, ищет этого пользователя в своей БД (и создает, если требуется) и возвращает на фронтенд access_token, с которым и происходят все дальнейшие запросы.

Для первого шага (получения AuthCode) в SPA существуют готовые реализации в виде, например, реакт-компонентов. Но под каждого OAuth-провайдера они разные, и подбор и настройка могут отнять достаточно много времени. После интеграции написанной библиотеки IdentityOAuthSpaExtensions (и настройки бэкэнд-части согласно инструкциям от майкрософта), запрос AuthCode из SPA будет состоять из двух частей:

  1. Создание обработчиков и подписка на события:
  2.     window.addEventListener("message", this.oAuthCodeReceived, false);
        function oAuthCodeReceived(message) {
            if (message.data && message.data.type === 'oauth-result') {
                if (data.code) {
                    externalAuthSuccess(data.provider, data.code);
                } else {
                    externalAuthError(data.provider, data.error, data.errorDescription);
                }
            }
        }
        function externalAuthSuccess(provider, code) {
            alert(`Provider: ${provider}, code: ${code}`);
        }
        function externalAuthError(provider, error, errorDescription) {
              alert(`Provider: ${provider}, error: ${error}, ${errorDescription}`);
        }
    
  3. Старт процедуры авторизации:
   window.open(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/external-auth/challenge?provider=${provider}`, undefined, 'toolbar=no,menubar=no,directories=no,status=no,width=800,height=600');

В результате этого ваше SPA получит AuthCode стороннего провайдера. В дальнейшем с ним можно делать что угодно ( :)), но в нашем случае, мы хотим получить access_token от нашего бэкэнда, чтобы в дальнейшем все http вызовы совершать с этим access_token’ом. Для этого в библиотеке существует возможность проверки AuthCode («), а также (рекомендуемая) интеграция с IdentityServer в виде extension grant’a. Описание интеграции очень подробно описано на гитхабе

Итоговая схема выглядит как-то так:

Из основных плюсов библиотеки:

  1. Единая точка входа и общий интерфейс интеграции любых Auth-провайдеров (не нужно менять SPA, меняется лишь одна переменная — имя провайдера — при открытии URL авторизации)
  2. Использование стороннего кода для взаимодействия с OAuth (саму библиотеку не придется обновлять, если в сторонних OAuth что-то изменится).
  3. Добавление новых провайдеров происходит стандартным способом (по инструкции для server-side приложений) и не требует модификации самой библиотеки

Пользуйтесь, задавайте вопросы и рассказывайте об успешных сценариях внедрения!

Доклад про Entity Framework Core на TomskDotNet#1

В конце ноября мы открыли сезон митапов TomskDotNet, и на первом из них я рассказал про особенности Entity Framework Core, типичные ошибки при работе с этой ORM, оптимизацию запросов и мониторинг.

Презентацию можно посмотреть ниже или скачать по ссылке.


С удовольствием приглашаю вас на наши последующие митапы (ближайший из которых — 20 марта).

Using Mini-Profiler with Angular and HttpClient

Performance is essential for every web app, so profiling is a must. And there’s no better tool to monitor and profile your web app than Mini-Profiler from StackExchange. It’s simple, easily integratable and provides the most important profile metrics such as request duration and SQL queries.

Our typical Web SPA setup in Rubius is based on ASP.Net Core backend and Angular frontend. So we started integrating MiniProfiler into the stack, but it wasn’t that straightforward.

The thing is, MiniProfiler works perfectly with classic pages and jquery ajax calls, but fails to display any information on Angular http requests, which makes it barely usable in SPA.
To overcome it, there is a perfect post from Georg Dangl on how to make Mini-Profiler work with Angular if you’re using HttpModule, so go read Georg’s post and gist, it’s for you :)

However, in 4.3 Angular introduced HttpClientModule as a new way to talk to the backend API (replacing old HttpModule) and once we started to migrate to it we had the same issue again. There were no information about ajax requests.
So, I sat down and ported Georg’s gist to an HttpClient. So if you care about your performance and SQL queries, go grab it!

Семинар по ASP.Net Core в Точке Кипения

14 декабря вместе с коллегой Антоном Финько выступали в Точке Кипения с семинаром по ASP.Net Core.

y_eKgPHhHbs
Очень понравилась сама площадка — Точка Кипения — это отличное место, просторный зал, огромный экран и все пришедшие 60 человек там отлично разместились (и даже если было бы вдвое больше — всё равно всем было бы удобно :)). Фотографии не передадут всего комфрота и уюта (и кофе-брейка с плюшками), но покажут, насколько было хорошо:
4kZgU9Ke01U

nZQgjVYFvvE

F_UDmu7bIW8

Это было первое IT-мероприятие в Точке, и я считаю, оно прошло отлично! Небольшой 40-минутный доклад про теоретические основы и большой практический опыт, и более чем получасовая дискуссия после. Очень порадовал обмен мнениями и опытом использования от многих присутствовавших.

Ну а тем, кто не пришел — видео и презентация ниже :)

AspnetCore.pptx

До встречи на следующих семинарах!

Realmius — бесплатная синхронизация данных между Realm и SQL Server

На ДевПро-2017 я рассказывал про мобильную базу данных Realm и преимуществах её использования в мобильных приложениях.

В докладе мы также говорили о синхронизации данных — это механизм, который пересылает добавленные/измененные объекты между клиентом и сервером, чтобы данные в клиентской и серверной базах совпадали.
Автоматическая синхронизация данных в мобильных приложениях очень удобна — она позволяет абстрагироваться от наличия/отсутствия интернета на устройстве и просто работать с локальной БД, читать и писать напрямую в неё. При этом получение новых данных или отправка данных на сервер отдается на откуп механизму синхронизации.

Realm имеет встроенный механизм синхронизации, однако он недостаточно удобен, в докладе на ДевПро я подробно останавливался на его минусах.

В Рубиусе мы начали активно использовать Realm в начале 2017-го, и, столкнувшись с недостатками встроенной синхронизации, решили этот момент улучшить.

Так появился Realmius — механизм синхронизации между Realm и SQL Server. В своих проектах мы его используем с марта (в том числе и в приложении DevPro), выложили на гитхаб в мае (как раз в преддверии ДевПро), и последние несколько месяцев активно улучшали документацию, чтобы сделать порог вхождения как можно ниже.

Почему стоит использовать Realmius? Всё просто:

  • Данные хранятся в SQL Server. Это значит, что на сервере можно выполнить любой SQL-запрос и получить произвольную аналитику по данным, использовать привычные инструменты для написания бэкэнда, бэкапы и все прочие плюшки проверенной и мощной серверной СУБД.
  • Возможность гибкой настройки прав доступа к данным
  • Автоматическая синхронизация: работайте с Realm на клиенте и EntityFramework на сервере привычным образом, синхронизация не накладывает никаких ограничений. Настройка синхронизации буквально в несколько строк
  • Бесплатность :)

Заинтересовало? Просто скачайте репозиторий и запустите наш простой пример! Когда захочется копнуть глубже в авторизацию и ограничения прав доступа — тут уже пригодится более продвинутый пример.

Realmius доступен через nuget, пара минут — и синхронизация уже интегрирована в ваше приложение! :) Подумайте, какие богатые возможности может добавить синхронизация данных в ваше новое мобильное приложение на Xamarin!
Кстати, Realmius можно использовать и в приложениях для Windows!

P.S. На днях пришло письмо из дружественной Индии — совершенно незнакомая нам компания нашла Realmius и начала его использовать. У них появились некоторые проблемы, которые в переписке мы успешно разрешили. Был очень удивлён, что без какого-либо пиара библиотека уже начинает набирать пользователей :)