Какие есть лучшие практики обновления моделей через брокер сообщений?
Недавно начал изучать микросервисный подход в создании распределенных систем, брокер RabbitMQ и библиотеку MassTransit для более высокоуровневого взаимодействия с шиной из под .NET Core.
Читая в интернете множество статей, часто натыкаюсь, что в сообщения, которыми между собой общаются микросервисы, часто кладут DTO той сущности, к которой имеет отношение некоторое наступившее событие.
Например, при изменении суммы в счете клиента, будет запаблишено сообщение AcoountChanged внутри которого будет лежать AccountDTO c актуальным срезом данных необходимой модели. Ну, а в других сервисах, где нужно, будет зарегистрирован Consumer потребляющий тип AcoountChanged. Соответственно на стороне потребителя я просто возьму данные из AccountDTO и заменю ими те, что лежат у него в базе. НО (собственно перехожу к сути вопроса) если по какой-то причине у меня есть два микросервиса, которые меняют информацию о счете клиента, и оба эти микросервиса паблишат периодически сообщение типа AcoountChanged, однако первый из них может полностью заполнить AcoountDTO, а второй не обладает полным набором данных о счете клиента, и скажем в некоторых полях он просто оставляет NULL, то на стороне потребителя мы не можем понять - этот NULL - значит, что значение изменилось на NULL и нам нужно взять его и проставить у себя в базе, или этот NULL означает что просто микросервис отправивший это сообщение ничего не знает про конкретно это поле и его не изменял...
Да, можно было бы, конечно, для подобных ситуаций вводить различие, например, по неймспейсам, и для каждого микросервиса делать свой тип сообщения AcoountChanged, чтобы как-то специальным образом их потом обрабатывать на стороне потребителя, но на мой взгляд это противоречит постулату о том, что каждый микросервис ничего не должен знать о других.
Надеюсь я не очень сумбурно обрисовал суть вопроса, над которым сейчас размышляю, а именно - каким образом лучше организовать и обрабатывать подобные ситуации.
Заранее спасибо и извиняюсь за многобуков.
Сразу оговорюсь, что в эту тему не погружался (не знаю каких-то особых подходов), но если рассуждать логически, то всё как-то так выглядит:
Нам нужно уметь различать, что какое-то свойство стало null, именно умышленно его в такое состояние переключили и отправили, а не просто нет о нём информации. Значит нужен какой-то механизм, который позволяет определить, как нам интерпретировать значение null на принимающей стороне.
Это C# 9.0
using System.Text.Json.Serialization;
#nullable enable
namespace ConsoleApp
{
public struct Optional<T>
{
private T? _value;
public Optional(T? value)
{
IsNewValue = true;
_value = value;
}
/// <summary>
/// Возвращает true, если в свойстве <see cref="Value"/>
/// находится новое значение, даже если это null.
/// </summary>
[JsonPropertyName("isNewValue")]
public bool IsNewValue { get; set; }
[JsonPropertyName("value")]
public T? Value
{
get => _value;
set
{
_value = value;
IsNewValue = true;
}
}
public override string ToString()
{
return $"{nameof(Value)}: {(Value is null ? "null" : Value.ToString())}, " +
$"{nameof(IsNewValue)}: {IsNewValue}";
}
public static implicit operator Optional<T>(T? value) => new(value);
}
}
using System.Text.Json.Serialization;
#nullable enable
namespace ConsoleApp
{
public class Message
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("description")]
public Optional<string> Description { get; set; }
[JsonPropertyName("serialNumber")]
public Optional<string> SerialNumber { get; set; }
[JsonPropertyName("owner")]
public Optional<string> Owner { get; set; }
}
}
using System;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
#nullable enable
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var message = new Message
{
Id = 100,
Title = "Название",
Description = "Описание",
SerialNumber = null
};
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic),
WriteIndented = true
};
try
{
string json = JsonSerializer.Serialize(message, options);
Console.WriteLine(json);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Если в классе Program присвоить свойству Description значение null, то это будет воспринято как новое значение, которым нужно перезаписать на принимающей стороне старое значение, а не как отсутствие информации.
Посмотрел уроки по RabbitMQ, посмотрел раздел Get Started на сайте MassTransit, пришёл в к выводу, что предложенное решение вполне применимо. Думал мало ли там какие-то особые форматы сообщений, какие-то особые подходы и мало ли что ещё, а по сути сериализация, десериализация и C# классы со свойствами соответственно.
Или можно отправлять инфу о свойствах, которым нужно присвоить null в сообщении, если такие свойства есть. Функционал всего этого дела можно написать в каком-нибудь базовом классе от которого наследуются все сообщения. Если этой инфы нет, то интерпретируем null, как свойство, которое нужно игнорировать. Это кажется даже немного сложнее, чем вариант выше, но тоже реализуемо и, скорее всего, потребует больше движений, чем просто определить свойство в виде обёртки нужного типа данных.
Борис Животное, Спасибо за заинтересованность. Применил этот подход, как самый очевидный, за не имением других вариантов. Правда возникла другая проблема. Но это уже не имеет отношения к данной теме.