惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
腾讯CDC
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
The Hacker News
The Hacker News
T
The Exploit Database - CXSecurity.com
B
Blog
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
T
Threatpost
博客园 - 聂微东
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
C
Check Point Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
爱范儿
爱范儿
IT之家
IT之家
S
Secure Thoughts
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
C
Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
Project Zero
Project Zero
博客园 - Franky
H
Heimdal Security Blog
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Подводные камни gRPC
poly_sladkiy · 2026-05-12 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение11 мин

Охват и читатели562

Ты только что получил задачу перенести сервис на gRPC - и думаешь, что это просто «взял protobuf, описал контракты, запустил кодогенерацию«? Так думал и я. До тех пор, пока не столкнулся с тем, что decimal в protobuf попросту не существует, nullable int превращается в нечто с флагом HasValue вместо привычного int?, а два enum в одном файле могут убить сборку из‑за конфликта имён значений — и компилятор скажет тебе об этом совсем не очевидно.»

Эта статья — выжимка боли и практики из реального проекта по переносу REST и WCF на gRPC, где моделей было больше сотни: с наследованием, дженериками, decimal, DateTime, object, nullable и совпадающими именами классов из разных пространств имён. Здесь не будет воды — только конкретные проблемы и конкретные решения. Обновлено под protoc v34.1 и.NET 10.

TL;DR;

  • Контракты — используй.proto‑файлы, а не protobuf‑net; один класс = один файл + package

  • Nullable для примитивов — optional int не даёт int?, нужны google/protobuf/wrappers.proto

  • Nullable для ссылочных типов и enum — кодогенератор не различает optional и без него; enum получает HasXxx/ClearXxx вместо Nullable<T>

  • Decimal — типа нет, используй DecimalValue по рецепту Microsoft или money.proto; избегай implicit operator

  • DateTime — используй google/protobuf/timestamp.proto; DateTime требует предварительного.ToUniversalTime()

  • Наследование — заменяется композицией; два подхода: от дочерних к родительским (читаемее) или от родительских к дочерним (удобнее при работе с базовым классом)

  • Дженерики — три стратегии: специализированные сообщения (безопаснее), oneof (компромисс), google.protobuf.Any (гибко, но без type safety)

  • Enum — имена значений должны быть уникальны в рамках всего файла; первое значение обязано быть 0; используй префикс <ИмяEnum>_<ИмяЗначения>

  • Object — четыре всадника апокалипсиса: Any, oneof, google.protobuf.Value/Struct, ручная сериализация в bytes/string

Описание контрактов: protobuf или не protobuf?

Проблема

gRPC говорит нам, что контракты могут быть описаны не только через protobuf. Я сейчас говорю про конкретную библиотеку protobuf-net. В нашей команде мы сравнивали protobuf и protobuf-net (.NET библиотека, которая позволяет добавить атрибуты вместо описания контрактов).

У нас было строгое требование, что обязательно должен быть contract-first, protobuf-net так может предоставить вам готовый protobuf контракт, но это не тоже самое, что иметь строго зафиксированные контракты в одном файле, чтобы другие команды могли использовать их и генерировать себе клиентов, особенно если они на других ЯП.

Решение

Рекомендуется использовать описание контрактов через proto-файлы.

Много ĸлассов в protobuf

Как я говорил, наша задача была переписать модели на Grpc, методов было немного, но количество классов переваливало за 100-ню. Изначально я писал все модели в одном proto-файле, но это длилось недолго, а именно до первых конфликтов имен. Затем я попытался группировать файлы с message по нейспейсам/запросам/возможно еще как-то, но ни к чему хорошему это не привело.

Решения

  1. Один класс = один proto-файл (рекомендуется) + обязательно используем package (аналог namespace в мире protobuf) для разграничения имён

  2. Однако, если количество классов по объему не столько большое как у нас, то можете разбить их по использованию в один файл

Подробнее можно почитать тут

Также полезно разбивать proto-файлы моделей и сервисов по папкам (например, Protos/Models/ и Protos/Services/ ), у вас получится примерно такой .csproj:

<ItemGroup>
  
  <!-- Модели: не привязаны ни к Server, ни к Client -->
  <Protobuf Include="Protos\Models\**\*.proto">
    <GrpcServices>None</GrpcServices>
    <Access>Public</Access>
    <ProtoCompile>True</ProtoCompile>
    <ProtoRoot>Protos</ProtoRoot> <!-- Корень откуда будут смотреть ваши import -->
  </Protobuf>
  
  <!-- Сервисы -->
  <Protobuf Include="Protos\Services\**\*.proto">
    <GrpcServices>Server</GrpcServices> <!-- Both - если вам нужен и клиент и сервер -->
    <Access>Public</Access>
    <ProtoCompile>True</ProtoCompile>
    <ProtoRoot>Protos</ProtoRoot>
    <CompileOutputs>True</CompileOutputs>
  </Protobuf>
  
</ItemGroup>

Самая полезная статья по Grpc.Tools здесь

Nullable для примитивов

Если значимое поле, например int32 SomeDataOptional, помечено как optional, то в сгенерированном классе не будет int? SomeDataOptional { get; set; }. Будет int SomeDataOptional { get; set; }, но будет свойство HasSomeData, которое обозначает, было ли передано данное поле или нет. Если обратиться к значению - там будет значение по умолчанию - это же значимый тип. Костыль, что сказать.

Решение - google/protobuf/wrappers.proto. При использовании оберток (wrappers) будет именно то, что нужно: nullable‑тип в сгенерированном коде.

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

Nullable для ссылочных типов

Для ссылочных типов (например, вложенных message ) ĸодогенератор не видит различий между optional и без него - оба варианта дадут свойство без ?.

Найди 10 отличий

Найди 10 отличий

Решение

Рекомендую все равно указывать optional для полей, которые могут быть null. Это важно для будущей логики кодогенерации и клиентов на других языках.

Nullable для перечислений (enum)

Ситуация аналогична значимым типам: optional enum добавит свойство HasXXX и метод ClearXXX , но никак не Nullable<EnumType>.

Решение

Подход

Результат

optional EnumType field

EnumType + HasXxx / ClearXxx

Обёртĸа

message EnumWrapper {
EnumType value = 1;

Да, это как с ссылочными типами, но иного выхода я не нашел. Тут также стоит указывать optional при использовании такого wrapper, следуя рекомендациям из прошлого правила

Выбирайте на свое усмотрение.

Decimal

В protobuf нет типа decimal. Прямые альтернативы - double и float , но они не подходят для финансовых вычислений из-за потери точности.

Решение

  1. google/protobuf/money.proto - содержит сумму и валюту (строĸа).

  2. DecimalValue по рецепту Microsoft

Важно: избегайте implicit operator для ĸонвертации, таĸ ĸаĸ это приводит ĸ NullReferenceException при DecimalValue == null. Используйте явные методы-расширения:

public static class DecimalValueExtension
{
    private const decimal NanoFactor = 1_000_000_000;
  
    public static decimal ToDecimal(this DecimalValue value)
      => value.Units + value.Nanos / NanoFactor;
  
    public static decimal? ToNullableDecimal(this DecimalValue? value)
      => value is null ? null : value.ToDecimal();
  
    public static DecimalValue ToDecimalValue(this decimal value)
    {
        var units = decimal.ToInt64(value);
        var nanos = decimal.ToInt32((value - units) * NanoFactor);
        return new DecimalValue { Units = units, Nanos = nanos };
    }
  
    public static DecimalValue? ToNullableDecimalValue(this decimal? value)
      => value is null ? null : value.Value.ToDecimalValue();
}

DateTime и DateTimeOffset

В protobuf также нет встроенного типа для работы с датой/временем.

Решение

Для работы с этими значениями используется google/protobuf/timestamp.proto, для C# у него имеется расширения:

  • Для Timestamp: ToDateTime() и ToDateTimeOffset()

  • В Timestamp:

    • Из DateTime: Для того, чтобы преобразовать DateTime в Timestamp необходимо сначала привести его к UTC, воспользовавшись методом ToUniversalTime(), а затем вызвать метод ToTimestamp()

    • Из DateTimeOffset: работает с .ToTimestamp() напрямую, без предварительных преобразований.

Наследование и абстраĸтные ĸлассы

В protobuf нет наследования, но есть композиция (и oneof, но об этом чуть позже). Мы нашли два варианта, как можно обойти наследование, оба построены на композиции, а вы что ожидали? В первом случае мы описываем от дочерних к родительским, и во втором, от родительских к дочерним - да есть и плюсы и минусы.

Исходная C#-иерархия для примера
[abstract] class Base
{
    public int Id { get; set; }
}
class User : Base
{
    public string Name { get; set; }
}
class UserAdditionalInfo : User
{
    public DateTimeOffset BDay { get; set; }
}
class Role : Base
{
    public int Priority { get; set; }
    public string RoleName { get; set; }
}
class UseBaseClass
{
    public Base Base { get; set; }
}

Решение №1: от дочерних к родительским (рекомендуется)

syntax = "proto3";

message Base {
    int32 id = 1;
}
message User {
    Base base = 1;
    string name = 2;
}
message UserAdditionalInfo {
    User base = 1;
    google.protobuf.Timestamp b_day = 2;
}
message Role {
    Base base = 1;
    int32 priority = 2;
    string role_name = 3;
}
// Нужен только если есть метод/класс, работающий с базовым типом
message BaseChildrens {
    oneof type {
        User user = 1;
        UserAdditionalInfo user_additional_info = 2;
        Role role = 3;
    }
}
message UseBaseClass {
    BaseChildrens base = 1;
}

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

Решение №2: от родительсĸих ĸ дочерним

syntax = "proto3";

// Если будет метод, который принимает базовый/абстрактный класс, то можно передать его
message Base {
    int32 id = 1;
    oneof child {
        User user = 2;
        Role role = 3;
    }
}
message User {
  string name = 1;
    oneof child {
        UserAdditionalInfo user_additional_info = 2;
    }
}
message UserAdditionalInfo {
    google.protobuf.Timestamp b_day = 1;
}
message Role {
    int32 priority = 1;
    string role_name = 2;
}
message UseBaseClass {
    Base base = 1; // все так же используется одна из реализаций
}

Сравнение

Решение №1: дочерние → родительсĸие

Решение №2: родительсĸие → дочерние

Читаемость иерархии

✅ Лучше

❌ Хуже

Работа с базовым ĸлассом

⚠️ Требуется BaseChildrens, если нужна работа с базовым классом

✅ Удобно, уже все есть

Generic, aka шаблонные типы

Записки в процессе работы с generic

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

В примере имеем шаблонный класс BaseEntity, в коде имеется применение с тремя разными типами. Есть три способа, как работать с ними, начнем с самой темной лошадки.

C#-исходниĸ
class BaseEntity<T>
{
    public T Id { get; set; }
}
class User : BaseEntity<int>
{
    // ... 
}
class Role : BaseEntity<long>
{ 
    // ... 
}
class BaseEntityWrapper
{
    public BaseEntity<string> BaseEntity { get; set; }
}

Решение №1: специализированные сообщения (рекомендуется)

Задай себе вопрос, ты знаешь во что превращаются дженерики?

Создать отдельное сообщение для каждой конкретной реализации generic. Да, это больно, да, это тяжело, надо пройти через отрицание к принятию.

syntax = "proto3";

message BaseEntity_Int {
    int32 id = 1;
}
message BaseEntity_Long {
    int64 id = 1;
}
message BaseEntity_String {
    string id = 1;
}
message User {
    BaseEntity_Int base = 1; // подход "от дочерних к родительским"
}
message Role {
    BaseEntity_Long base = 1;
}
message BaseEntityWrapper {
    BaseEntity_String base_entity = 1; // здесь не наследование
}

Решение №2: полиморфизм через объединение (oneof)

message BaseEntity {
    oneof id {
        int32  id_int32  = 1;
        int64  id_int64  = 2;
        string id_string = 3;
    }
}

message BaseEntityWrapper {
    BaseEntity base_entity = 1;
}

При работе с oneof надо проверять на соответствие каждому возможному типу перед тем как записать значение.

var proto = new BaseEntity();

if (entity is BaseEntity<int> intEntity)
    proto.IdInt32 = intEntity.Id;
else if (entity is BaseEntity<long> longEntity)
    proto.IdInt64 = longEntity.Id;
else if (entity is BaseEntity<string> strEntity)
    proto.IdString = strEntity.Id;

Решение №3: полная динамика (google.protobuf.Any)

Any позволяет заполнить любой тип, как это делает object, подробнее тут.

import "google/protobuf/any.proto";

message BaseEntityWrapper {
    google.protobuf.Any base_entity = 1;
}

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

// Pack
var userEntity = new UserEntity { Id = 42 };
var wrapper = new BaseEntityWrapper
{
    BaseEntity = Any.Pack(userEntity)
};

// Unpack
if (wrapper.BaseEntity.Is(UserEntity.Descriptor))
{
    var user = wrapper.BaseEntity.Unpack<UserEntity>();
}

Сравнение

Подход

Type Safety

Гибкость

Специализированные сообщения

✅ Высокая

❌ Низкая

oneof

⚠️ Средняя

⚠️ Средняя

Any

❌ Низкая

✅ Высокая

Enum: именование и нулевое значение

Проблема №1: ĸонфлиĸт имён значений

В protobuf значения enum используют C++ scoping rules - имена значений должны быть униĸальны в рамĸах всего паĸета (файла), а не тольĸо внутри одного enum. Пример ниже вызовет ошибку.

syntax = "proto3";

enum PositionTypes {
    Up = 1;
    Down = 2; // злодей № 1 !!!
    Both = 3;
}
enum GraphMovementTypes {
    Upper = 0;
    Down = 1; // злодей № 2 !!! - не обязательно должны совпадать значения
    None = 2;
}

Решение

Именовать значения по правилу <ИмяEnum>_<ИмяЗначения>

syntax = "proto3";

enum PositionTypes {
    PositionTypes_Up = 0;
    PositionTypes_Down = 10;
    PositionTypes_Both = 20;
}
enum GraphMovementTypes {
    GraphMovementTypes_Upper = 0;
    GraphMovementTypes_Down = 1;
}

По итогу у вас значения enum не будут содержать имени самого enum

Об этом правиле также говориться на сайте документации, используйте именно такой подход для именования значений перечислений.

Проблема 2: enum в protobuf всегда начинается с 0

По правилам protobuf3 первое значение enum обязано быть 0. При переносе legacy-ĸода, где нумерация начинается с 1, не смещайте существующие значения - добавьте <ИмяEnum>_Undefined = 0.

Object

Я нашел 4 основных способа, как работать с этим демоном, конечно же я про них расскажу, но с единственной оговоркой - у нас на бекенде object поле никак не обрабатывалось, оно приходило и уходило, поэтому мы приняли просто решение заставить стерилизовать значения клиентов и выбрали string.

Решение №1: google.protobuf.Any

Да снова он, это буквально прямой аналог object, клади всё, что хочешь.

import "google/protobuf/any.proto";

message Container {
    google.protobuf.Any value = 1;
}

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

// Pack
var container = new Container();
container.Value = Any.Pack(new UserEntity { Id = 1 });

// Unpack - нужно знать тип заранее
if (container.Value.Is(UserEntity.Descriptor))
{
    var user = container.Value.Unpack<UserEntity>();
}

// Unpack - через type_url если тип неизвестен
var typeUrl = container.Value.TypeUrl; // "type.googleapis.com/UserEntity"

Кстати говоря, можно сделать, чтобы значение передавалось со схемой, тогда клиент сможет парсить её самостоятельно.

Решение №2: union через oneof

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

message DynamicValue {
    oneof value {
        int32   int_val    = 1;
        int64   long_val   = 2;
        double  double_val = 3;
        string  str_val    = 4;
        bool    bool_val   = 5;
        bytes   bytes_val  = 6;
    }
}

message Container {
    DynamicValue value = 1;
}
// Запись
var val = new DynamicValue();
val.StrVal = "hello";          // object str = "hello"
val.IntVal = 42;               // object num = 42

// Чтение
var result = val.ValueCase switch
{
    DynamicValue.ValueOneofCase.StrVal    => (object)val.StrVal,
    DynamicValue.ValueOneofCase.IntVal    => (object)val.IntVal,
    DynamicValue.ValueOneofCase.DoubleVal => (object)val.DoubleVal,
    _ => null
};

Решение №3: JSON-подобная структура google.protobuf.Value

Встроенный тип для динамических данных, аналог JsonElement. Поддерживает: null, bool, double, string, list, struct (map).

import "google/protobuf/struct.proto";

message Container {
    google.protobuf.Value value  = 1;  // любой скалярный тип
    google.protobuf.Struct config = 2; // аналог Dictionary<string, object>
    google.protobuf.ListValue tags = 3; // аналог List<object>
}
// Value — скаляры
var container = new Container();
container.Value = Value.ForString("hello");
container.Value = Value.ForNumber(3.14);
container.Value = Value.ForBool(true);
container.Value = Value.ForNull();

// Struct — как Dictionary<string, object>
container.Config = new Struct();
container.Config.Fields["name"]  = Value.ForString("Alice");
container.Config.Fields["age"]   = Value.ForNumber(30);
container.Config.Fields["roles"] = Value.ForList(
    Value.ForString("admin"),
    Value.ForString("user")
);

// Чтение
var kind = container.Value.KindCase;  // StringValue, NumberValue, etc.
var name = container.Config.Fields["name"].StringValue;

Решение №4: ручная сериализация bytes или string

Тут все просто - сериализуете так, как вам удобнее.

Сравнение

Подход

Что можно положить

Type Safety

Производительность

Гибкость

Any

Protobuf-сообщения

⚠️ Средняя

⚠️ Средняя

✅ Высокая

oneof

Фиксированный набор

✅ Высокая

✅ Высокая

❌ Низкая

Value/Struct

JSON-примитивы

❌ Низкая

⚠️ Средняя

✅ Высокая

bytes/string

Любые

❌ Низкая

✅ Высокая

✅ Максимальная

Список полезных материалов

  1. Как работать с библиотекой Grpc.Tools: Protocol Buffers/gRPC Codegen Integration Into .NET Build

  2. Рекомендации по структуре файлов proto: 1-1-1 Best Practice

  3. Лучшие практики proto: Proto Best Practices

  4. Работа с генератором кода protoc: C# Generated Code Guide

  5. Типы данных C# и Proto: Create Protobuf messages for .NET apps