Реализация 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.

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

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