Форум программистов, компьютерный форум, киберфорум
stackOverflow
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

CRUD API на C# и GraphQL

Запись от stackOverflow размещена 05.05.2025 в 21:32
Показов 4526 Комментарии 0
Метки .net, api, c#, graphql

Нажмите на изображение для увеличения
Название: 2832307e-6e63-472c-b97b-c2443b681ade.jpg
Просмотров: 227
Размер:	187.8 Кб
ID:	10748
В бэкенд-разработке постоянно возникают новые технологии, призванные решить актуальные проблемы и упростить жизнь программистам. Одной из таких технологий стал GraphQL — язык запросов для API, который постепенно набирает всё большую популярность и становится реальной альтернативой традиционным REST-интерфейсам. Если вы ещё не нырнули в этот прекрасный мир гибких запросов и точно заточенных ответов — самое время это исправить.

GraphQL был разработан компанией Facebook в 2012 году и вышел в опен-сорс в 2015-м, но по-настоящему расцвел именно в последние годы. Почему? Причина в том, что он предлагает принципиально иной подход к организации взаимодействия между клиентом и сервером, который отлично вписывается в современные требования к разработке веб-приложений. В отличие от REST, который оперирует фиксированными эндпоинтами и заранее определёнными структурами данных, GraphQL позволяет клиенту самому указать, какие именно данные ему нужны. Представьте ситуацию: у вас есть мобильное приложение, которому для отображения профиля пользователя нужны только имя и аватарка. А есть веб-версия, которой требуется полная информация, включая контакты, настройки и историю активности. В мире REST вы бы создали два отдельных эндпоинта или один, возвращающий все данные, часть из которых оказалась бы лишней для мобильного клиента. С GraphQL подход иной — клиент сам описывает, какие поля ему нужны, а сервер возвращает данные точно в запрошенном формате. Это не только экономит трафик, но и устраняет проблемы недополучения (under-fetching) или переполучения (over-fetching) данных.

Звучит заманчиво, не так ли? Но GraphQL — это не просто альтернатива REST, это целая экосистема инструментов и подходов. И когда речь заходит об использовании GraphQL в экосистеме .NET и C#, у нас есть несколько мощных библиотек, которые делают интеграцию максимально безболезненной. Главный игрок на этом поле — HotChocolate. Эта библиотека предоставляет полноценный сервер GraphQL для .NET, включая поддержку запросов, мутаций, подписок, валидации, авторизации и многого другого. Что особенно ценно, HotChocolate прекрасно интегрируется с Entity Framework Core, позволяя создавать эффективные API, тесно связанные с вашей моделью данных.

Другой интересный вариант — GraphQL.NET, который предоставляет немного иной подход к определению схемы и обработке запросов. Есть и другие библиотеки, каждая со своими преимуществами и особенностями, но в рамках этой статьи мы сфокусируемся именно на HotChocolate как на наиболее зрелом и полнофункциональном решении.

В чём же конкретные преймущества GraphQL перед REST, кроме уже упомянутой возможности запрашивать ровно те данные, которые нужны? Вот несколько ключевых моментов:

1. Единый эндпоинт — вместо множества URL для разных ресурсов, GraphQL использует единую точку входа, что упрощает управление API.
2. Строго типизированная схема — каждое поле в GraphQL имеет конкретный тип, что обеспечивает надёжность и возможность автоматической проверки запросов.
3. Встроенная документация — благодаря системе интроспекции, схема GraphQL самодокументируется, и клиенты могут автоматически получать информацию о структуре данных.
4. Упрощённая версионность — добавление новых полей не ломает существующие клиенты, что снижает проблемы версионирования API.
5. Эффективная загрузка связанных данных — GraphQL позволяет в одном запросе получить не только основные данные, но и связанные ресурсы, что ликвидирует проблему N+1 запросов.

Впрочем, у каждой технологии есть свои компромисы и GraphQL — не исключение. Мы обязательно обсудим и потенциальные недостатки в следующих разделах. Но сейчас давайте сосредоточимся на том, как начать использовать эту технологию для создания гибких и эффективных CRUD API.

Потенциальные недостатки GraphQL и сценарии применения REST



Несмотря на все преимущества GraphQL, было бы наивно полагать, что эта технология идеальна для всех сценариев. Как разработчик, я не раз обжигался на слепом следовании модным трендам. Давайте честно поговорим о ситуациях, когда GraphQL может оказаться не лучшим выбором.

Во-первых, сложность кэширования. Если в REST мы можем легко кэшировать ответы на уровне URL, то с единой точкой входа GraphQL это становится проблематичным. Кэширование приходится реализовывать на уровне самого приложения или использовать специальные инструменты вроде Apollo Cache или Relay. А ведь хороший кэш — половина успеха высоконагруженного API. Причем эта проблема усугубляется тем, что клиенты могут запрашивать абсолютно любые комбинации полей. Представьте, у вас миллион возможных комбинаций запросов — будете кэшировать каждую? Технически это возможно, но на практике довольно трудозатратно.

Не стоит забывать и о дополнительной нагрузке на серверную часть. GraphQL-запросы требуют более сложной обработки, чем простые REST-вызовы. Это особенно заметно при работе с большими объемами данных или сложными взаимосвязанными структурами. Однажды я столкнулся с ситуацией, когда неаккуратно составленный запрос практически положил наш сервер, вызвав лавину обращений к базе данных. Кроме того, GraphQL по умолчанию не устанавливает ограничений на запросы. Без дополнительных механизмов защиты клиент может сформировать запрос, который вызовет чрезмерную нагрузку на сервер или даже DOS-атаку. В одном из проэктов нам пришлось срочно внедрять ограничения по глубине и сложности запросов после того, как один из "умных" фронтендеров решил получить всё одним запросом.

Есть и ситуации, когда REST просто более практичен:

1. Простые CRUD-операции с предсказуемой структурой данных. Если ваше API используется только для базовых операций, и все клиенты запрашивают одни и те же наборы полей, излишняя гибкость GraphQL становится ненужным усложнением.
2. Работа с бинарными данными. Хотя в GraphQL есть возможность передавать файлы, это требует дополнительных настроек и часто менее удобно, чем прямая отправка через REST.
3. Микросервисная архитектура начального уровня. Если у вас только зарождающаяся микросервисная инфраструктура, начинать с GraphQL может быть преждевременно. REST обычно проще в реализации и отладке для отдельных сервисов.
4. Публичные API для сторонних разработчиков. Не всегда можно расчитывать на то, что внешние разработчики знакомы с GraphQL. REST остаётся более общепринятым и понятным большинству.
5. Высоконагруженные системы с жёсткими требованиями к производительности. В таких случаях оптимизированный REST-эндпоинт под конкретную задачу может работать эффективнее, чем универсальный GraphQL.

Я помню один проект финтех-приложения, где мы начали с GraphQL, но затем выделили несколько критичных операций в отдельные REST-эндпоинты. Причина банальна — нам нужна была максимальная предсказуемость и производительность для операций перевода денег, а гибкость запросов в этом случае только мешала. В конечном счёте, выбор между GraphQL и REST — это не религиозная война, а прагматичное решение, основанное на конкретных требованиях вашего проекта. Иногда оптимальным оказывается даже гибридный подход, когда часть функциональности реализуется через GraphQL, а часть — через традиционный REST.

MVC: CRUD подобное API для моделей – best practices?
Доброго времени суток! Долго думал куда постить – в "PHP и ООП" или "Для начинающих" ибо вопрос...

WCF служба. Целесообразность CRUD сервиса, обобщенный API для нескольких сущностей
Здравствуйте Очередной раз вернулся к разбору WCF. Первый вопрос который меня волнует это на...

CRUD WEB API with Entity Framework
Я начал разрабатывать свой проект - веб-приложение с базой данных. Я использовал WEB API с Entity...

Регистрация, операции Crud в asp web api
Здравствуйте, мне нужно реализовать простое веб-приложение. Серверную часть пишу на asp web api....


Эволюция API: от SOAP к REST и далее к GraphQL



В начале 2000-х бал правил SOAP (Simple Object Access Protocol) — монструозный, основанный на XML протокол обмена структурированными сообщениями. Помню те времена, когда приходилось иметь дело с гигантскими WSDL-файлами, описывающими каждую мелочь контракта API. SOAP был порождением эпохи корпоративной разработки — строгий, формальный, избыточный, но предсказуемый. У него были свои преимущества: строгая типизация, богатый набор стандартов для безопасности (WS-Security) и транзакционности (WS-Transaction). Однако вместе с этим шли и недостатки: сложность, вербозность и трудности с отладкой. Работать с SOAP без специализированных инструментов было настоящей пыткой.

Примерно к 2008-2010 годам начался массовый переход на REST (Representational State Transfer). В отличие от SOAP, REST не был протоколом — скорее философией проектирования, основанной на использовании стандартных возможностей HTTP. Вместо сложных XML-конвертов — простые URL, понятные HTTP-методы (GET, POST, PUT, DELETE) и легкие форматы данных, в основном JSON. Я помню, как заходил в первый раз в Postman и отправлял GET-запрос на RESTful API. Это казалось магией — никаких XML-конвертов, никаких сложных настроек заголовков. Просто адрес и нажатие кнопки "Send". REST произвел революцию, сделав API более доступными и менее формальными. Он идеально вписался в эпоху мобильных приложений и JavaScript-фреймворков. Но и у него были свои проблемы: недополучение или переполучение данных, множество эндпоинтов для связанных ресурсов, сложности с версионированием.

К 2015 году, когда Facebook выпустил GraphQL в опен-сорс, эти проблемы стали особенно острыми. Мир стал более мобильным, клиенты — более разнообразными, а требования к гибкости API — более высокими. GraphQL предложил новый взгляд: единый эндпоинт, декларативные запросы и точный контроль над получаемыми данными. Этот подход оказался настолько удачным для определённых сценариев, что за несколько лет GraphQL из эксперимента превратился в мейнстрим, который поддерживается гигантами вроде GitHub, Shopify, Twitter и, конечно, самого Facebook (ныне Meta). Интересно, что ни одна новая технология полностью не вытеснила предыдущую. SOAP до сих пор живёт в корпоративных системах, REST остаётся наиболее распространённым стандартом для публичных API, а GraphQL нашёл свою нишу в сложных клиент-серверных приложениях.

Эволюция продолжается и дальше: мы видим появление gRPC для высокопроизводительных микросервисов, WebSockets и Server-Sent Events для реал-тайм коммуникаций, новые подходы к работе с асинхронными API. Но GraphQL сегодня находится на пике зрелости — достаточно зрелая технология, чтобы использоваться в промышленных масштабах, но все еще достаточно свежая, чтобы не обрасти грузом устаревших практик.

Основы GraphQL



Стержнем любого GraphQL API является схема (schema). По сути, это контракт между клиентом и сервером, который определяет, какие данные и операции доступны. Схема состоит из типов и операций, которые можно выполнять над этими типами. В отличие от REST, где структура ответа определяется сервером, в GraphQL клиент сам указывает, какие именно поля ему нужны в рамках определённой схемы.

Типы в GraphQL образуют иерархическую структуру и бывают нескольких видов:

1. Скалярные типы — базовые типы данных, такие как Int, Float, String, Boolean и ID. Это кирпичики, из которых строятся более сложные структуры.

2. Объектные типы — определяют набор полей, каждое из которых имеет свой тип. Например:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
type Book {
  id: ID!
  title: String!
  author: Author!
  publicationYear: Int
}
 
type Author {
  id: ID!
  name: String!
  books: [Book!]!
}
Обратите внимание на восклицательный знак (!) — он обозначает, что поле не может быть null. А квадратные скобки ([]) указывают на массив значений.

3. Перечислимые типы (Enum) — ограниченный набор возможных значений:

JSON
1
2
3
4
5
6
enum Genre {
  FICTION
  NON_FICTION
  SCIENCE
  HISTORY
}
4. Интерфейсы и объединения — механизмы для полиморфизма в GraphQL.
5. Входные типы — специальные типы для использования в аргументах операций.

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

Запросы (Queries) — операции, которые только читают данные без изменений на сервере. Запросы в GraphQL декларативны — клиент явно указывает, какие поля ему нужны:

JSON
1
2
3
4
5
6
7
8
query {
  book(id: "123") {
    title
    author {
      name
    }
  }
}
Здесь клиент запрашивает книгу с id "123" и хочет получить только название книги и имя автора. Сервер вернёт JSON, точно соответствующий структуре запроса:

JSON
1
2
3
4
5
6
7
8
9
10
{
  "data": {
    "book": {
      "title": "Война и мир",
      "author": {
        "name": "Лев Толстой"
      }
    }
  }
}
Мутации (Mutations) — операции, которые изменяют данные на сервере. Синтаксически они похожи на запросы, но начинаются с ключевого слова mutation:

JSON
1
2
3
4
5
6
mutation {
  addBook(title: "1984", authorId: "456") {
    id
    title
  }
}
Этот запрос создаст новую книгу и вернёт её id и название. Мутации всегда выполняются последовательно, в отличие от запросов, которые могут выполняться параллельно.

Подписки (Subscriptions) — механизм для получения уведомлений о изменениях данных в реальном времени. Они особенно полезны для чатов, уведомлений и других сценариев, требующих обновлений в реальном времени:

JSON
1
2
3
4
5
6
7
8
subscription {
  bookAdded {
    title
    author {
      name
    }
  }
}
Эта подписка будет отправлять уведомление каждый раз, когда добавляется новая книга.

В контексте C# и HotChocolate, вы реализуете схему GraphQL, определяя C# классы, которые соответствуют типам в вашей схеме. Например, объектный тип Book может быть представлен так:

C#
1
2
3
4
5
6
7
public class Book
{
    public string Id { get; set; }
    public string Title { get; set; }
    public Author Author { get; set; }
    public int? PublicationYear { get; set; }
}
А запрос для получения книги может выглядеть так:

C#
1
2
3
4
5
6
7
public class Query
{
    public Book GetBook(string id, [Service] IBookRepository repository)
    {
        return repository.GetBookById(id);
    }
}
HotChocolate интегрируется с ASP.NET Core через middleware, который вы добавляете в вашу конфигурацию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddGraphQLServer()
        .AddQueryType<Query>()
        .AddMutationType<Mutation>();
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGraphQL();
    });
}
Таким образом, HotChocolate анализирует ваши классы и автоматически создаёт соответствующую схему GraphQL. Это значительно упрощает интеграцию GraphQL в существующие C# проекты, особенно если вы уже используете Entity Framework Core или другие ORM.

Стоит особо отметить, что одно из главных отличий GraphQL от REST в том, как решается проблема N+1 запросов. В REST, если вы хотите получить список книг с их авторами, вам потребуется сначала запросить список книг, а затем для каждой книги делать отдельный запрос для получения информации об авторе. В GraphQL вы можете получить всё в одном запросе:

JSON
1
2
3
4
5
6
7
8
query {
  books {
    title
    author {
      name
    }
  }
}
Однако на стороне сервера эта проблема всё ещё существует — для каждой книги потенциально делается отдельный запрос к базе данных для получения автора. Решения этой проблемы мы обсудим в следующих разделах, когда будем говорить о DataLoader и других механизмах оптимизации.

Ещё одно важное понятие в GraphQL — резолверы (resolvers). Это функции, которые определяют, как получить данные для конкретного поля. В HotChocolate резолверы обычно представлены методами C# класса. Например:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Field(a => a.Books)
            .ResolveWith<Resolvers>(r => r.GetBooks(default!, default!))
            .UseDbContext<AppDbContext>();
    }
 
    private class Resolvers
    {
        public IEnumerable<Book> GetBooks(Author author, [ScopedService] AppDbContext context)
        {
            return context.Books.Where(b => b.AuthorId == author.Id);
        }
    }
}
Здесь мы определяем, как получить список книг для конкретного автора. Это особенно полезно, когда вам нужна логика, которая выходит за рамки простого доступа к свойствам.

В дополнение к резолверам, GraphQL предлагает механизм директив, которые можно рассматривать как метаданные или инструкции для системы выполнения запросов. Директивы изменяют выполнение запроса или мутации и обозначаются знаком @. Например, директива @include позволяет включать поле в результат только при выполнении определённого условия:

JSON
1
2
3
4
5
6
7
8
query GetBookDetails($includeAuthor: Boolean!) {
  book(id: "123") {
    title
    author @include(if: $includeAuthor) {
      name
    }
  }
}
Такой запрос вернёт информацию об авторе только если переменная $includeAuthor равна true. Это мощный инструмент для создания гибких и контролируемых запросов.

Другая важная директива — @deprecated, которая помечает поле как устаревшее:

JSON
1
2
3
4
5
6
type Book {
  id: ID!
  title: String!
  oldField: String @deprecated(reason: "Используйте newField")
  newField: String
}
В экосистеме C# и HotChocolate директивы реализуются как отдельные классы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequireAdminDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("requireAdmin");
        descriptor.Location(DirectiveLocation.Field);
        descriptor.Use(next => context =>
        {
            // Проверка прав администратора
            if (!context.User.IsInRole("Admin"))
            {
                throw new UnauthorizedAccessException("Доступ запрещен");
            }
            return next(context);
        });
    }
}
Затем вы регистрируете эту директиву при настройке сервера:

C#
1
2
3
4
services
    .AddGraphQLServer()
    .AddDirectiveType<RequireAdminDirectiveType>()
    .AddQueryType<Query>();
Важно понимать, что GraphQL — декларативный, а не императивный. Вы не указываете, *как* получить данные, вы просто декларируете, *что* вам нужно получить. Это ключевая философская разница с REST, где URL и метод HTTP определяют, как именно сервер должен обработать запрос. Ещё одна особенность GraphQL — встроенная система валидации. Сервер автоматически проверяет запросы на соответствие схеме. Если клиент запрашивает несуществующее поле или передаёт аргумент неправильного типа, сервер вернёт ошибку ещё до выполнения запроса. Это значительно упрощает отладку и делает API более надёжным.

Система типов GraphQL также поддерживает концепцию интерфейсов и объединений (unions). Интерфейсы определяют набор полей, которые должны быть у типа:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Named {
  name: String!
}
 
type Book implements Named {
  name: String!
  author: Author!
}
 
type Author implements Named {
  name: String!
  books: [Book!]!
}
А объединения позволяют полю возвращать значения разных типов:

JSON
1
2
3
4
5
union SearchResult = Book | Author
 
type Query {
  search(query: String!): [SearchResult!]!
}
В C# с HotChocolate это реализуется через классы-маркеры и явное указание типов в схеме:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface INamed
{
    string Name { get; }
}
 
public class Book : INamed
{
    public string Name { get; set; }
    // Другие поля
}
 
public class SearchResultType : UnionType
{
    protected override void Configure(IUnionTypeDescriptor descriptor)
    {
        descriptor.Name("SearchResult");
        descriptor.Type<BookType>();
        descriptor.Type<AuthorType>();
    }
}
Сравнение с REST выходит за рамки просто технических различий. Разработка с использованием GraphQL требует и другого мышления. Если в REST мы думаем категориями ресурсов и эндпоинтов, то в GraphQL мышление ориентированно на типы данных и их связи. Это особенно ценно, когда ваше API должно обслуживать разные клиенты с разными потребностями в данных. При проэктировании GraphQL API важно избегать искушения создать один огромный объектный тип, включающий все возможные поля. Лучше разделить схему на логические части и использовать интерфейсы и объединения для создания гибкой структуры. Это не только упростит поддержку API в долгосрочной перспективе, но и сделает его более понятным для клиентов.

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

Интроспекция и продвинутые возможности GraphQL



GraphQL скрывает под капотом множество инструментов, которые делают жизнь разработчика значительно проще. Одна из самых мощных — система интроспекции, позволяющая API исследовать само себя. Представьте, что вы пришли в незнакомую библиотеку и мгновенно получили детальный каталог всех книг с описаниями — именно так работает интроспекция в GraphQL.
Технически интроспекция — это спецальный запрос, позволяющий получить информацию о схеме API. Например, для получения списка всех типов можно выполнить:

JSON
1
2
3
4
5
6
7
8
9
{
  __schema {
    types {
      name
      kind
      description
    }
  }
}
Эта возможность не просто удобство — она критически важна для создания инструментов вроде GraphQL Playground или GraphiQL, которые предоставляют интерактивную документацию и возможность тестировать запросы прямо в браузере. В мире REST для этого понадобились бы внешние инструменты вроде Swagger. В C# с HotChocolate интроспекция включена по умолчанию, и вы можете настроить её так:

C#
1
2
3
4
5
6
7
services.AddGraphQLServer()
    .AddQueryType<Query>()
    .ModifyOptions(options =>
    {
        options.EnableSchemaRequests = true; // Включает запросы схемы через GET
        options.EnableIntrospection = true;  // Включает интроспекцию
    });
Ещё одна мощная фича GraphQL — фрагменты. Они позволяют определить набор полей, которые можно переиспользовать в разных запросах:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fragment BookDetails on Book {
  id
  title
  publicationYear
}
 
query {
  book(id: "1") {
    ...BookDetails
    author {
      name
    }
  }
  recommendedBooks {
    ...BookDetails
  }
}
Фрагменты особенно полезны, когда у вас есть сложная схема с множеством связанных типов. Вместо копирования одних и тех же полей в разных местах запроса, вы определяете их один раз и переиспользуете.

Не менее интересны условные фрагменты, которые используются с директивами @include и @skip:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
query GetBookDetails($includeAuthor: Boolean!) {
  book(id: "1") {
    ...BookBasics
    ...AuthorDetails @include(if: $includeAuthor)
  }
}
 
fragment BookBasics on Book {
  id
  title
}
 
fragment AuthorDetails on Book {
  author {
    name
    biography
  }
}
Этот запрос вернёт информацию об авторе, только если переменная $includeAuthor имеет значение true. Такой подход позволяет клиенту гибко контролировать объём получаемых данных без необходимости создавать отдельные запросы для каждого сценария.
Инлайн-фрагменты (inline fragments) необходимы при работе с интерфейсами и объединениями. Они помогают уточнить, какие поля запрашивать для конкретного типа:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
query {
  search(query: "толстой") {
    __typename
    ... on Book {
      title
      author { name }
    }
    ... on Author {
      name
      books { title }
    }
  }
}
Здесь мы запрашиваем разные поля в зависимости от того, является ли результат поиска книгой или автором. Поле __typename — ещё одна встроенная возможность GraphQL, которая возвращает имя типа конкретного объекта.
Еще одна продвинутая возможность GraphQL — пакетные запросы (batch queries). HotChocolate позволяет отправлять несколько операций в одном HTTP-запросе:

JSON
1
2
3
4
5
6
7
8
[
  {
    "query": "query { book(id: \"1\") { title } }"
  },
  {
    "query": "mutation { addBook(title: \"Новая книга\") { id } }"
  }
]
Это особенно полезно для мобильных приложений, где важна экономия трафика и снижение количества сетевых запросов.
Расширенные возможности директив в GraphQL не ограничиваются стандартными @include и @skip. В HotChocolate вы можете определять собственные директивы для различных целей — от авторизации до трансформации данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UppercaseDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("uppercase");
        descriptor.Location(DirectiveLocation.Field);
        descriptor.Use(next => context =>
        {
            var result = next(context);
            if (result is string value)
            {
                return value.ToUpper();
            }
            return result;
        });
    }
}
Эта директива преобразует строковые значения полей в верхний регистр. После регистрации:

C#
1
2
services.AddGraphQLServer()
    .AddDirectiveType<UppercaseDirectiveType>()
Её можно использовать в запросе:

JSON
1
2
3
4
5
{
  book(id: "1") {
    title @uppercase
  }
}
Мощь GraphQL ещё и в том, как он решает проблему версионирования API. Вместо создания новых URL с суффиксами /v2/, /v3/ и т.д., GraphQL поощряет эволюционный подход: добавление новых полей и пометку устаревших полей директивой @deprecated.
Такой подход значительно снижает "боль" при обновлениях API, позволяя клиентам постепенно адаптироваться к изменениям без необходимости полного переписывания своего кода.

В HotChocolate пометить поле как устаревшее можно так:

C#
1
2
3
4
5
6
7
8
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.OldField)
            .Deprecated("Используйте NewField вместо этого");
    }
}

Пошаговая реализация CRUD API



Давайте перейдём от слов к делу и реализуем полноценное CRUD API на базе C# и GraphQL. Мы создадим простую, но функциональную систему для работы с книгами и авторами, которую потом можно будет расширить под ваши конкретные задачи. Первый шаг — создание нового проекта. Я предпочитаю начинать с пустого шаблона ASP.NET Core Web API:

Bash
1
2
dotnet new webapi -n BookstoreGraphQL
cd BookstoreGraphQL
Теперь необходимо установить нужные пакеты NuGet. Для нашего проекта понадобятся:

Bash
1
2
3
4
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
HotChocolate — наш основной инструмент для работы с GraphQL в .NET. Пакеты Entity Framework понадобятся для взаимодействия с базой данных.

Следующий этап — определение моделей данных. Создадим директорию Models и добавим туда наши классы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Book.cs
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string ISBN { get; set; }
    public DateTime PublicationDate { get; set; }
    public int AuthorId { get; set; }
    
    public Author Author { get; set; }
}
 
// Author.cs
public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Biography { get; set; }
    
    public ICollection<Book> Books { get; set; } = new List<Book>();
}
Теперь создадим контекст базы данных. Добавим класс AppDbContext.cs в директорию Data:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
 
    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Настраиваем связи между сущностями
        modelBuilder.Entity<Book>()
            .HasOne(b => b.Author)
            .WithMany(a => a.Books)
            .HasForeignKey(b => b.AuthorId);
            
        // Добавляем пример данных для тестирования
        modelBuilder.Entity<Author>().HasData(
            new Author { Id = 1, Name = "Джордж Оруэлл", Biography = "Британский писатель и журналист" },
            new Author { Id = 2, Name = "Федор Достоевский", Biography = "Русский писатель, мыслитель" }
        );
 
        modelBuilder.Entity<Book>().HasData(
            new Book { Id = 1, Title = "1984", ISBN = "978-5-17-086661-1", AuthorId = 1, PublicationDate = new DateTime(1949, 6, 8) },
            new Book { Id = 2, Title = "Скотный двор", ISBN = "978-5-17-086662-8", AuthorId = 1, PublicationDate = new DateTime(1945, 8, 17) },
            new Book { Id = 3, Title = "Преступление и наказание", ISBN = "978-5-17-086663-5", AuthorId = 2, PublicationDate = new DateTime(1866, 1, 1) }
        );
    }
}
С моделями разобрались, теперь зарегистрируем наши сервисы в Program.cs (или Startup.cs для более старых версий .NET):

C#
1
2
3
4
5
6
7
8
9
10
11
12
// Регистрация DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
 
// Регистрация GraphQL
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddType<BookType>()
    .AddType<AuthorType>()
    .RegisterDbContext<AppDbContext>();
Не забудьте добавить строку подключения в appsettings.json:

JSON
1
2
3
4
5
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BookstoreGraphQL;Trusted_Connection=True"
  }
}
Теперь самая интересная часть — реализация GraphQL-типов и запросов. Создадим директорию GraphQL и добавим туда наши типы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// GraphQL/Types/BookType.cs
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Description("Представляет книгу в системе");
        
        descriptor.Field(b => b.Id).Description("Уникальный идентификатор книги");
        descriptor.Field(b => b.Title).Description("Название книги");
        descriptor.Field(b => b.ISBN).Description("Международный стандартный книжный номер");
        descriptor.Field(b => b.PublicationDate).Description("Дата публикации");
        
        descriptor.Field(b => b.Author)
            .ResolveWith<Resolvers>(r => r.GetAuthor(default!, default!))
            .UseDbContext<AppDbContext>()
            .Description("Автор книги");
    }
    
    private class Resolvers
    {
        public Author GetAuthor(Book book, [ScopedService] AppDbContext context)
        {
            return context.Authors.FirstOrDefault(a => a.Id == book.AuthorId);
        }
    }
}
 
// GraphQL/Types/AuthorType.cs
public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Description("Представляет автора книг");
        
        descriptor.Field(a => a.Id).Description("Уникальный идентификатор автора");
        descriptor.Field(a => a.Name).Description("Имя автора");
        descriptor.Field(a => a.Biography).Description("Биография автора");
        
        descriptor.Field(a => a.Books)
            .ResolveWith<Resolvers>(r => r.GetBooks(default!, default!))
            .UseDbContext<AppDbContext>()
            .Description("Книги, написанные автором");
    }
    
    private class Resolvers
    {
        public IEnumerable<Book> GetBooks(Author author, [ScopedService] AppDbContext context)
        {
            return context.Books.Where(b => b.AuthorId == author.Id);
        }
    }
}
Теперь определим запросы, которые позволят получать данные из нашей системы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// GraphQL/Query.cs
public class Query
{
    [UseDbContext(typeof(AppDbContext))]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
    {
        return context.Books;
    }
    
    [UseDbContext(typeof(AppDbContext))]
    public async Task<Book> GetBookAsync([ScopedService] AppDbContext context, int id)
    {
        return await context.Books.FindAsync(id);
    }
    
    [UseDbContext(typeof(AppDbContext))]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Author> GetAuthors([ScopedService] AppDbContext context)
    {
        return context.Authors;
    }
    
    [UseDbContext(typeof(AppDbContext))]
    public async Task<Author> GetAuthorAsync([ScopedService] AppDbContext context, int id)
    {
        return await context.Authors.FindAsync(id);
    }
}
Обратите внимание на атрибуты [UseProjection], [UseFiltering] и [UseSorting]. Они добавляют мощные возможности, которых нет в стандартном REST API:

UseProjection — позволяет клиентам запрашивать только нужные им поля,
UseFiltering — добавляет возможность фильтрации данных прямо в запросе,
UseSorting — добавляет возможность сортировки данных.

Не забудьте зарегистрировать эти возможности в Program.cs:

C#
1
2
3
4
5
6
7
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddProjections()
    .AddFiltering()
    .AddSorting();
Для примера, вот как может выглядеть запрос, который использует эти возможности:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
query {
  books(
    where: { title: { contains: "1984" } }
    order: { publicationDate: ASC }
  ) {
    id
    title
    publicationDate
    author {
      name
    }
  }
}
Этот запрос найдёт все книги, содержащие в названии "1984", отсортирует их по дате публикации и вернёт только указанные поля.
Осталось созать миграции для базы данных и применить их:

Bash
1
2
dotnet ef migrations add InitialCreate
dotnet ef database update
Запустим приложение:

Bash
1
dotnet run
По умолчанию, HotChocolate включает GraphQL Playground — интерактивную IDE для работы с GraphQL. Откройте браузер и перейдите по адресу https://localhost:7001/graphql (порт может отличаться). Вы увидите интерфейс, где можно выполнять запросы к вашему API и изучать его документацию.

Теперь, когда мы настроили запросы для чтения данных, пора реализовать остальную часть CRUD-функциональности: создание, обновление и удаление. В GraphQL эти операции реализуются через мутации. Создадим класс Mutation.cs в директории GraphQL:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// GraphQL/Mutation.cs
public class Mutation
{
    // Создание новой книги
    [UseDbContext(typeof(AppDbContext))]
    public async Task<Book> AddBookAsync(
        [ScopedService] AppDbContext context,
        AddBookInput input)
    {
        var book = new Book
        {
            Title = input.Title,
            ISBN = input.ISBN,
            PublicationDate = input.PublicationDate,
            AuthorId = input.AuthorId
        };
 
        context.Books.Add(book);
        await context.SaveChangesAsync();
 
        return book;
    }
 
    // Обновление существующей книги
    [UseDbContext(typeof(AppDbContext))]
    public async Task<Book> UpdateBookAsync(
        [ScopedService] AppDbContext context,
        UpdateBookInput input)
    {
        var book = await context.Books.FindAsync(input.Id);
        
        if (book == null)
            throw new GraphQLException($"Книга с ID {input.Id} не найдена");
 
        book.Title = input.Title ?? book.Title;
        book.ISBN = input.ISBN ?? book.ISBN;
        
        if (input.PublicationDate.HasValue)
            book.PublicationDate = input.PublicationDate.Value;
            
        if (input.AuthorId.HasValue)
            book.AuthorId = input.AuthorId.Value;
 
        await context.SaveChangesAsync();
 
        return book;
    }
 
    // Удаление книги
    [UseDbContext(typeof(AppDbContext))]
    public async Task<bool> DeleteBookAsync(
        [ScopedService] AppDbContext context,
        int id)
    {
        var book = await context.Books.FindAsync(id);
        
        if (book == null)
            return false;
 
        context.Books.Remove(book);
        await context.SaveChangesAsync();
 
        return true;
    }
 
    // Аналогичные методы для авторов
    [UseDbContext(typeof(AppDbContext))]
    public async Task<Author> AddAuthorAsync(
        [ScopedService] AppDbContext context,
        AddAuthorInput input)
    {
        var author = new Author
        {
            Name = input.Name,
            Biography = input.Biography
        };
 
        context.Authors.Add(author);
        await context.SaveChangesAsync();
 
        return author;
    }
 
    // Другие методы для авторов...
}
Кроме самих методов-мутаций, нам понадобятся входные типы для передачи данных. Создадим их в отдельном файле:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GraphQL/Types/BookInputs.cs
public record AddBookInput(
    string Title,
    string ISBN,
    DateTime PublicationDate,
    int AuthorId);
 
public record UpdateBookInput(
    int Id,
    string? Title = null,
    string? ISBN = null,
    DateTime? PublicationDate = null,
    int? AuthorId = null);
 
// GraphQL/Types/AuthorInputs.cs
public record AddAuthorInput(
    string Name,
    string? Biography = null);
 
public record UpdateAuthorInput(
    int Id,
    string? Name = null,
    string? Biography = null);
Обратите внимание, что для операций обновления мы используем необязательные параметры (nullable), чтобы можно было обновлять только нужные поля.
Теперь можно протестировать мутации. Вот примеры запросов для GraphQL Playground:

Добавление нового автора:
JSON
1
2
3
4
5
6
7
8
9
10
11
mutation {
  addAuthor(
    input: {
      name: "Джоан Роулинг"
      biography: "Британская писательница, автор серии книг о Гарри Поттере"
    }
  ) {
    id
    name
  }
}
Добавление новой книги:
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mutation {
  addBook(
    input: {
      title: "Гарри Поттер и философский камень"
      isbn: "978-5-353-00308-3"
      publicationDate: "1997-06-26"
      authorId: 3  # ID автора, полученный при создании
    }
  ) {
    id
    title
    author {
      name
    }
  }
}
Обновление книги:
JSON
1
2
3
4
5
6
7
8
9
10
11
12
mutation {
  updateBook(
    input: {
      id: 4
      title: "Гарри Поттер и философский камень (обновлено)"
    }
  ) {
    id
    title
    publicationDate
  }
}
Удаление книги:
JSON
1
2
3
mutation {
  deleteBook(id: 4)
}
Прелесть GraphQL в том, что он позволяет комбинировать операции и выбирать, какие именно данные вернуть после выполнения мутации. Например, после создания книги мы можем сразу запросить её автора с дополнительными деталями:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mutation {
  addBook(
    input: {
      title: "Новая книга"
      isbn: "123-456-789"
      publicationDate: "2023-01-01"
      authorId: 1
    }
  ) {
    id
    title
    author {
      name
      biography
      books {
        title
      }
    }
  }
}
Такой запрос не только создаст новую книгу, но и вернёт информацию об авторе, включая список всех его книг.
Один из нюансов реализации GraphQL API — обработка ошибок. В нашем примере мы использовали GraphQLException для случаев, когда запрашиваемая книга не найдена. HotChocolate автоматически преобразует это исключение в понятный клиенту ответ:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "errors": [
    {
      "message": "Книга с ID 999 не найдена",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "updateBook"
      ]
    }
  ],
  "data": {
    "updateBook": null
  }
}
Для более сложных сценариев можно реализовать кастомный обработчик ошибок:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomErrorFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
        if (error.Exception is DbUpdateException)
        {
            return error.WithMessage("Ошибка при сохранении в базу данных");
        }
        
        return error;
    }
}
 
// Регистрация в Program.cs
builder.Services
    .AddGraphQLServer()
    .AddErrorFilter<CustomErrorFilter>();
Часто требуется валидация входных данных. HotChocolate поддерживает интеграцию с FluentValidation:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Установите пакет
// dotnet add package HotChocolate.FluentValidation
 
public class AddBookInputValidator : AbstractValidator<AddBookInput>
{
    public AddBookInputValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
        RuleFor(x => x.ISBN).Matches(@"^\d{3}-\d{3}-\d{3}$").WithMessage("ISBN должен иметь формат XXX-XXX-XXX");
        RuleFor(x => x.PublicationDate).LessThanOrEqualTo(DateTime.Today);
    }
}
 
// Регистрация в Program.cs
builder.Services
    .AddGraphQLServer()
    .AddFluentValidation();
Теперь, если клиент попытается создать книгу с недопустимыми значениями, он получит подробную информацию об ошибках валидации.

Наше API готово и полностью функционально, но у GraphQL есть еще множество продвинутых возможностей, которые стоит изучить для создания профессиональных приложений. В следующих разделах мы рассмотрим интеграцию с Entity Framework Core, реализацию авторизации, пагинацию и другие аспекты разработки GraphQL API.

Интеграция с Entity Framework Core и реализация мутаций



Тесная интеграция GraphQL с ORM — один из ключевых моментов, который делает разработку API действительно приятной. В экосистеме .NET нет лучшего кандидата на роль ORM, чем Entity Framework Core. HotChocolate осознал это и предоставил глубокую интеграцию с EF Core через пакет HotChocolate.Data.EntityFramework.
Когда мы регистрируем наш DbContext с помощью метода .RegisterDbContext<AppDbContext>(), за кулисами происходит магия, позволяющая прозрачно использовать контекст в резолверах:

C#
1
2
3
4
5
[UseDbContext(typeof(AppDbContext))]
public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
{
    return context.Books;
}
Обратите внимание на атрибуты [UseDbContext] и параметр [ScopedService]. Они обеспечивают, что каждый запрос получит правильный экземпляр контекста из DI-контейнера. Это решает один из наиболее распространённых подводных камней в ASP.NET Core — использование DbContext в нескольких параллельных запросах.
Для усложнения мутаций часто требуется выполнять операции в рамках транзакции. Например, если мы создаём книгу и одновременно хотим обновить статистику автора, нам нужно гарантировать атомарность этой операции:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[UseDbContext(typeof(AppDbContext))]
public async Task<Book> AddBookWithAuthorUpdateAsync(
    [ScopedService] AppDbContext context,
    AddBookWithStatsInput input)
{
    // Важно: начинаем транзакцию
    using var transaction = await context.Database.BeginTransactionAsync();
    
    try
    {
        var book = new Book
        {
            Title = input.Title,
            ISBN = input.ISBN,
            PublicationDate = input.PublicationDate,
            AuthorId = input.AuthorId
        };
 
        context.Books.Add(book);
        
        // Обновляем статистику автора
        var author = await context.Authors.FindAsync(input.AuthorId);
        if (author == null)
            throw new GraphQLException($"Автор с ID {input.AuthorId} не найден");
            
        author.BookCount = await context.Books.CountAsync(b => b.AuthorId == input.AuthorId) + 1;
        author.LastBookPublished = input.PublicationDate;
        
        await context.SaveChangesAsync();
        
        // Если всё прошло успешно — коммитим транзакцию
        await transaction.CommitAsync();
        
        return book;
    }
    catch
    {
        // При любой ошибке — откатываем изменения
        await transaction.RollbackAsync();
        throw;
    }
}
Такой подход гарантирует целостность данных даже в сложных операциях.
При работе с Entity Framework важно помнить о проблеме N+1 запросов. Допустим, мы запрашиваем список книг с их авторами:

JSON
1
2
3
4
5
6
7
8
9
query {
  books {
    id
    title
    author {
      name
    }
  }
}
Наивная реализация этого запроса привела бы к одному запросу для получения всех книг и затем отдельному запросу для каждой книги, чтобы получить её автора. Для 100 книг это уже 101 запрос к базе данных! HotChocolate предлагает несколько решений этой проблемы. Первое — использование .Include() в запросе:

C#
1
2
3
4
5
[UseDbContext(typeof(AppDbContext))]
public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
{
    return context.Books.Include(b => b.Author);
}
Это простое решение, но оно всегда загружает авторов, даже если клиент их не запрашивает.
Более элегантное решение — использовать DataLoader. Он автоматически батчит несколько однотипных запросов в один и кэширует результаты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AuthorDataLoader : BatchDataLoader<int, Author>
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;
 
    public AuthorDataLoader(
        IDbContextFactory<AppDbContext> contextFactory,
        IBatchScheduler batchScheduler)
        : base(batchScheduler)
    {
        _contextFactory = contextFactory;
    }
 
    protected override async Task<IReadOnlyDictionary<int, Author>> LoadBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken cancellationToken)
    {
        await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
        
        return await context.Authors
            .Where(a => keys.Contains(a.Id))
            .ToDictionaryAsync(a => a.Id, cancellationToken);
    }
}
А теперь обновим резолвер для получения автора книги:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.Author)
            .ResolveWith<Resolvers>(r => r.GetAuthor(default!, default!));
    }
    
    private class Resolvers
    {
        public async Task<Author> GetAuthor(
            Book book,
            AuthorDataLoader authorLoader)
        {
            return await authorLoader.LoadAsync(book.AuthorId);
        }
    }
}
Теперь, если клиент запросит 100 книг с их авторами, DataLoader сделает всего 2 запроса: один для получения книг и один для получения всех авторов этих книг.
При реализации более сложных мутаций могут пригодиться входные типы с валидацией. Например, для создания книги с одновременным созданием автора (если он ещё не существует):

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public record CreateBookWithAuthorInput(
    string Title,
    string ISBN,
    DateTime PublicationDate,
    AuthorInput Author);
 
public record AuthorInput(
    int? ExistingAuthorId,
    string? NewAuthorName,
    string? NewAuthorBiography);
 
public class CreateBookWithAuthorInputValidator : AbstractValidator<CreateBookWithAuthorInput>
{
    public CreateBookWithAuthorInputValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
        RuleFor(x => x.ISBN).NotEmpty().Matches(@"^[\d-]+$");
        
        RuleFor(x => x.Author.ExistingAuthorId)
            .NotNull()
            .When(x => string.IsNullOrEmpty(x.Author.NewAuthorName))
            .WithMessage("Необходимо указать либо ID существующего автора, либо информацию о новом авторе");
            
        RuleFor(x => x.Author.NewAuthorName)
            .NotEmpty()
            .When(x => x.Author.ExistingAuthorId == null)
            .WithMessage("Имя автора обязательно для нового автора");
    }
}
Такая валидация обеспечивает, что клиент предоставит либо ID существующего автора, либо информацию для создания нового. Добавим соотвествующую мутацию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[UseDbContext(typeof(AppDbContext))]
public async Task<Book> CreateBookWithAuthorAsync(
    [ScopedService] AppDbContext context,
    CreateBookWithAuthorInput input)
{
    using var transaction = await context.Database.BeginTransactionAsync();
    
    try
    {
        int authorId;
        
        // Определяем ID автора (существующего или нового)
        if (input.Author.ExistingAuthorId.HasValue)
        {
            // Проверяем существование автора
            var author = await context.Authors.FindAsync(input.Author.ExistingAuthorId.Value);
            if (author == null)
                throw new GraphQLException($"Автор с ID {input.Author.ExistingAuthorId.Value} не найден");
                
            authorId = input.Author.ExistingAuthorId.Value;
        }
        else
        {
            // Создаём нового автора
            var newAuthor = new Author
            {
                Name = input.Author.NewAuthorName!,
                Biography = input.Author.NewAuthorBiography
            };
            
            context.Authors.Add(newAuthor);
            await context.SaveChangesAsync();
            
            authorId = newAuthor.Id;
        }
        
        // Создаём книгу
        var book = new Book
        {
            Title = input.Title,
            ISBN = input.ISBN,
            PublicationDate = input.PublicationDate,
            AuthorId = authorId
        };
        
        context.Books.Add(book);
        await context.SaveChangesAsync();
        
        await transaction.CommitAsync();
        
        return book;
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}
Эта мутация демонстрирует важный паттерн при работе с GraphQL и EF Core: транзакционность, валидация входных данных и гибкая обработка связанных сущностей. Именно такие возможности делают GraphQL особенно мощным инструментом для сложных операций изменения данных.

Авторизация и пагинация в GraphQL API



Безопасность API — это та область, где компромисы непозволительны. Даже самая элегантная и гибкая система бесполезна, если не защищена должным образом. С GraphQL ситуация усложняется тем, что вместо набора эндпоинтов с чётко определёнными правами доступа, у нас единая точка входа с гибкой структурой запросов. HotChocolate предлагает удобный механизм авторизации, интегрированный со стандартной системой ASP.NET Core Identity. Начнём с простейшего случая — защиты всей схемы или отдельных операций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Регистрация сервисов аутентификации в Program.cs
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
});
 
// Не забываем включить middleware аутентификации и авторизации
app.UseAuthentication();
app.UseAuthorization();
Теперь мы можем применять авторизацию к нашим типам и резолверам. Простейший вариант — добавить атрибут [Authorize]:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Authorize]
public class Mutation
{
    // Только авторизованные пользователи могут выполнять мутации
}
 
public class Query
{
    [Authorize]
    public Book GetBookInternal(int id, [ScopedService] AppDbContext context)
    {
        // Только авторизованные пользователи могут получить эту книгу
    }
    
    [Authorize(Roles = "Admin")]
    public async Task<bool> DeleteBook(int id, [ScopedService] AppDbContext context)
    {
        // Только администраторы могут удалять книги
    }
}
Более интересно реализовать тонкую гранулярную защиту на уровне отдельных полей или типов. Например, мы можем скрыть чувствительные данные от неавторизованных пользователей:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.ISBN)
            .Authorize(new[] { "Admin", "Editor" })
            .Description("ISBN книги (доступно только админам и редакторам)");
            
        descriptor.Field(b => b.InternalNotes)
            .Authorize("Admin")
            .Description("Внутренние заметки (только для админов)");
    }
}
Но иногда авторизация требует более гибкой логики. Возможно, мы хотим, чтобы авторы могли редактировать только свои книги. Здесь пригодится кастомная политика авторизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Настройка политики в Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BookAuthor", policy =>
        policy.RequireAssertion(context =>
        {
            // Здесь будет логика проверки, является ли текущий пользователь автором книги
            return true; // Упрощено для примера
        }));
});
 
// Применение политики в мутации
[Authorize("BookAuthor")]
public async Task<Book> UpdateBook(int id, UpdateBookInput input, [ScopedService] AppDbContext context)
{
    // Реализация обновления книги
}
Особенно интересны возможности директив в GraphQL для авторизации. Мы можем создать кастомную директиву @requireAuth:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RequireAuthDirectiveType : DirectiveType
{
    protected override void Configure(IDirectiveTypeDescriptor descriptor)
    {
        descriptor.Name("requireAuth");
        descriptor.Location(DirectiveLocation.Field);
        descriptor.Use(next => context =>
        {
            if (!context.User.Identity.IsAuthenticated)
            {
                throw new UnauthorizedException("Требуется авторизация");
            }
            return next(context);
        });
    }
}
 
// Регистрация директивы
builder.Services
    .AddGraphQLServer()
    .AddDirectiveType<RequireAuthDirectiveType>();
Теперь клиенты могут использовать эту директиву прямо в запросах:

JSON
1
2
3
4
query {
  sensitiveField @requireAuth
  publicField
}
Перейдём к пагинации. Когда ваша база содержит тысячи записей, загружать их все одним запросом — плохая идея. GraphQL предлагает несколько подходов к пагинации, но самый популярный — пагинация в стиле Relay (Cursor-based pagination).
В HotChocolate реализовать её просто:

C#
1
2
3
4
5
6
7
8
9
public class Query
{
    [UseDbContext(typeof(AppDbContext))]
    [UsePaging]  // Вот она, магия пагинации!
    public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
    {
        return context.Books;
    }
}
Атрибут [UsePaging] автоматически трансформирует ваш обычный резолвер в резолвер с поддержкой пагинации в стиле Relay. Теперь клиенты могут делать такие запросы:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query {
  books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}
Здесь first: 10 указывает, что нам нужно первые 10 элементов, а after — курсор, после которого следует начинать выборку. В ответе каждый элемент получает свой курсор, а pageInfo содержит метаданные для навигации.
Для более гибкой настройки пагинации можно использовать расширенные параметры:

C#
1
2
3
4
5
6
7
8
9
[UsePaging(
    MaxPageSize = 50,          // Максимальное количество элементов на странице
    DefaultPageSize = 10,       // Размер страницы по умолчанию
    IncludeTotalCount = true    // Включать общее количество элементов
)]
public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
{
    return context.Books;
}
Теперь клиенты могут получать дополнительную информацию о общем количестве элементов:

JSON
1
2
3
4
5
6
7
8
9
10
query {
  books(first: 10) {
    totalCount
    edges {
      node {
        title
      }
    }
  }
}
Когда авторизация и пагинация используются вместе, получается мощное сочетание. Например, мы можем обеспечить, чтобы пользователи видели только те книги, к которым у них есть доступ, и при этом эффективно загружали их порциями:

C#
1
2
3
4
5
6
7
8
9
10
11
12
[Authorize]
[UsePaging]
[UseFiltering]
public IQueryable<Book> GetAccessibleBooks(
    [ScopedService] AppDbContext context,
    [Service] IUserAccessService accessService)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var accessibleBookIds = accessService.GetAccessibleBookIds(userId);
    
    return context.Books.Where(b => accessibleBookIds.Contains(b.Id));
}
Примерно так стоит подходить к реализации безопасности и пагинации в любом серьёзном GraphQL API на C#.

Тестирование и оптимизация



Разработка API — лишь половина дела. Без тщательного тестирования и оптимизации даже самый элегантный GraphQL API может превратиться в источник постоянной головной боли. К счастью, экосистема вокруг GraphQL предлагает богатый набор инструментов для выявления и устранения потенциальных проблем.

Начнём с тестирования. В отличие от REST, где для каждого эндпоинта нужен отдельный тест, GraphQL позволяет сосредоточиться на тестировании бизнес-логики и резолверов. В .NET для этого есть несколько подходов.
Модульное тестирование резолверов можно проводить с помощью стандартных инструментов вроде xUnit или NUnit:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BookResolverTests
{
  [Fact]
  public async Task GetBook_ExistingId_ReturnsBook()
  {
      // Arrange
      var dbContextMock = new Mock<AppDbContext>();
      var bookId = 1;
      var expectedBook = new Book { Id = bookId, Title = "Тестовая книга" };
      
      dbContextMock.Setup(db => db.Books.FindAsync(bookId))
          .ReturnsAsync(expectedBook);
      
      var query = new Query();
      
      // Act
      var result = await query.GetBookAsync(dbContextMock.Object, bookId);
      
      // Assert
      Assert.Equal(expectedBook, result);
  }
}
Но такие тесты слишком сильно привязаны к реализации. Более реалистичный подход — интеграционное тестирование с использованием HotChocolate.TestUtils, который позволяет выполнять реальные GraphQL-запросы к схеме:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BookQueryIntegrationTests : IClassFixture<TestServerFixture>
{
  private readonly TestServerFixture _fixture;
  
  public BookQueryIntegrationTests(TestServerFixture fixture)
  {
      _fixture = fixture;
  }
  
  [Fact]
  public async Task GetBook_ExistingId_ReturnsBook()
  {
      // Arrange
      var query = @"
          query {
              book(id: 1) {
                  id
                  title
              }
          }";
      
      // Act
      var result = await _fixture.ExecuteRequestAsync(query);
      
      // Assert
      result.MatchSnapshot(); // Сравнение с сохранённым эталонным ответом
  }
}
Класс TestServerFixture в этом примере настраивает тестовый сервер с нашей GraphQL-схемой:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class TestServerFixture : IDisposable
{
  private readonly TestServer _server;
  private readonly HttpClient _client;
  
  public TestServerFixture()
  {
      var hostBuilder = new WebHostBuilder()
          .ConfigureServices(services =>
          {
              // Настраиваем сервисы, используя тестовую БД
              services.AddDbContext<AppDbContext>(options =>
                  options.UseInMemoryDatabase("TestDb"));
                  
              services.AddGraphQLServer()
                  .AddQueryType<Query>()
                  .AddMutationType<Mutation>();
                  
              // Добавляем тестовые данные
              using var scope = services.BuildServiceProvider().CreateScope();
              var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
              SeedTestData(context);
          })
          .Configure(app =>
          {
              app.UseRouting();
              app.UseEndpoints(endpoints => endpoints.MapGraphQL());
          });
          
      _server = new TestServer(hostBuilder);
      _client = _server.CreateClient();
  }
  
  private void SeedTestData(AppDbContext context)
  {
      // Заполняем базу тестовыми данными
      context.Authors.Add(new Author { Id = 1, Name = "Тестовый Автор" });
      context.Books.Add(new Book { Id = 1, Title = "Тестовая книга", AuthorId = 1 });
      context.SaveChanges();
  }
  
  public async Task<IExecutionResult> ExecuteRequestAsync(string query)
  {
      var request = new HttpRequestMessage(HttpMethod.Post, "/graphql")
      {
          Content = new StringContent(
              JsonSerializer.Serialize(new { query }),
              Encoding.UTF8,
              "application/json")
      };
      
      var response = await _client.SendAsync(request);
      var content = await response.Content.ReadAsStringAsync();
      
      return QueryRequestBuilder.New()
          .SetQuery(query)
          .Create()
          .Execute(
              await _server.Services.GetRequiredService<ISchema>().CompileAsync());
  }
  
  public void Dispose()
  {
      _client.Dispose();
      _server.Dispose();
  }
}
Такой подход позволяет тестировать GraphQL API максимально приближенно к реальным условиям, но с контролируемой средой и данными. Для ручного тестирования и отладки незаменимы инструменты вроде GraphQL Playground или Insomnia. Они позволяют конструировать запросы, изучать схему с помощью встроенной интроспекции и анализировать ответы. При работе с HotChocolate Playground доступен по умолчанию на пути /graphql.

Теперь перейдём к оптимизации. Первая и главная проблема GraphQL — защита от сложных запросов. Представьте, что клиент отправляет глубоко вложенный запрос:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
{
author(id: 1) {
  books {
    author {
      books {
        author {
          # И так далее до бесконечности...
        }
      }
    }
  }
}
}
Такой запрос может вызвать лавину обращений к базе и положить сервер. Решение — ограничение глубины и сложности запросов:

C#
1
2
3
4
5
6
7
8
9
services.AddGraphQLServer()
  .AddQueryType<Query>()
  .SetParsedDocumentCachingOptions(
      new ParseDocumentCachingOptions
      {
          MaxDocumentSize = 1024 * 20, // Макс. размер запроса - 20KB
      })
  .AddMaxExecutionDepthRule(10)  // Максимальная глубина запроса - 10 уровней
  .AddComplexityRule(200);       // Максимальная расчетная сложность - 200 единиц
За этими простыми строками скрывается мощный механизм анализа и ограничения сложности запросов. HotChocolate автоматически вычисляет "стоимость" каждого запроса и отклоняет те, которые превышают установленные лимиты.

Ещё одна типичная проблема — уже упомянутая N+1 проблема. Представим, что у нас есть запрос всех авторов и их книг:

JSON
1
2
3
4
5
6
7
8
{
authors {
  name
  books {
    title
  }
}
}
Наивная реализация сделает один запрос для получения авторов и затем отдельный запрос для книг каждого автора. Если у нас 100 авторов, это приведёт к 101 запросу к базе! Мы уже обсуждали решение через DataLoader, но давайте углубимся в детали:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BooksDataLoader : GroupedDataLoader<int, Book>
{
  private readonly IDbContextFactory<AppDbContext> _contextFactory;
  
  public BooksDataLoader(
      IDbContextFactory<AppDbContext> contextFactory,
      DataLoaderOptions options) : base(options)
  {
      _contextFactory = contextFactory;
  }
  
  protected override async Task<ILookup<int, Book>> LoadGroupedBatchAsync(
      IReadOnlyList<int> authorIds,
      CancellationToken cancellationToken)
  {
      await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
      
      var books = await context.Books
          .Where(b => authorIds.Contains(b.AuthorId))
          .ToListAsync(cancellationToken);
          
      return books.ToLookup(b => b.AuthorId);
  }
}
А в нашем типе Author используем этот DataLoader:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AuthorType : ObjectType<Author>
{
  protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
  {
      descriptor.Field(a => a.Books)
          .ResolveWith<Resolvers>(r => r.GetBooks(default!, default!));
  }
  
  private class Resolvers
  {
      public async Task<IEnumerable<Book>> GetBooks(
          Author author,
          BooksDataLoader dataLoader)
      {
          return await dataLoader.LoadAsync(author.Id);
      }
  }
}
Теперь при выполнении запроса со 100 авторами будет выполнено всего 2 запроса к базе: один для получения авторов и один для получения всех необходимых книг.
Важный момент в оптимизации — мониторинг производительности. HotChocolate предлагает интеграцию с OpenTelemetry для сбора метрик и трассировки:

C#
1
2
3
services.AddOpenTelemetryTracing(builder => builder
  .AddHotChocolateInstrumentation()
  .AddZipkinExporter());
Это позволяет отслеживать время выполнения запросов, задержки в резолверах и выявлять проблемные места.
Для оптимизации часто полезно анализировать планы выполнения запросов SQL, особенно когда GraphQL взаимодействует с реляционной БД. Можно включить логирование SQL-запросов:

C#
1
2
3
4
services.AddDbContext<AppDbContext>(options =>
  options.UseSqlServer(connectionString)
         .EnableSensitiveDataLogging()
         .LogTo(Console.WriteLine, LogLevel.Information));
Это поможет выявить неэффективные запросы, которые нуждаются в оптимизации через индексы или переписывание.
Одна из хитростей, которую я часто применяю — использование проекций для исключения ненужных JOIN-ов:

C#
1
2
3
4
5
[UseProjection]
public IQueryable<Book> GetBooks([ScopedService] AppDbContext context)
{
  return context.Books;
}
Атрибут [UseProjection] позволяет клиенту управлять SQL-запросом, автоматически добавляя или исключая JOIN-ы в зависимости от запрашиваемых полей. Если клиент запрашивает только заголовки книг без авторов, JOIN к таблице авторов не будет выполнен.

Для более глубокого анализа производительности GraphQL API критически важно проводить нагрузочное тестирование. В отличие от REST, где каждый эндпоинт можно тестировать отдельно с предсказуемой нагрузкой, GraphQL запросы могут сильно различаться по сложности. Я часто использую k6 с расширением для GraphQL:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import http from 'k6/http';
import { check } from 'k6';
 
export default function() {
  const query = `
    query {
      books(first: 20) {
        edges {
          node {
            title
            author {
              name
            }
          }
        }
      }
    }
  `;
  
  const response = http.post('https://api.example.com/graphql', 
    JSON.stringify({ query: query }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  
  check(response, {
    'is status 200': (r) => r.status === 200,
    'no errors': (r) => !JSON.parse(r.body).errors,
    'has data': (r) => JSON.parse(r.body).data !== null
  });
}
Такое тестирование поможет выявить узкие места в вашем API под нагрузкой. Однажды именно этот подход помог мне обнаружить, что при конкурентных запросах наш DataLoader работал неэффективно из-за неправильной настройки пула потоков.

Одна из самых мощных стратегий оптимизации — кэширование. HotChocolate поддерживает несколько уровней кэширования. Начнём с кэширования запросов:

C#
1
2
3
4
5
6
7
services.AddGraphQLServer()
  .AddQueryType<Query>()
  .AddCaching(options =>
  {
      options.EnableQueryCache = true;
      options.MaxQueryCacheSize = 1000;
  });
Это позволяет кэшировать результаты выполнения частых запросов. Но можно пойти дальше и кэшировать отдельные резолверы:

C#
1
2
3
4
5
6
[NodeResolver]
[UseCaching(maxAge: 60)] // Кэширование на 60 секунд
public async Task<Book> GetBookById(int id, BookByIdDataLoader dataLoader)
{
    return await dataLoader.LoadAsync(id);
}
На практике я обнаружил, что схема кэширования зависит от конкретного домена. Для данных, которые редко меняются (справочники, исторические записи), можно использовать более агрессивное кэширование. Для часто обновляемых данных лучше либо отключить кэширование, либо установить очень короткий TTL.

Сетевая оптимизация тоже важна. Включите сжатие HTTP для уменьшения размера ответов:

C#
1
2
3
4
5
6
7
8
app.UseResponseCompression();
 
services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});
Особенно это актуально для GraphQL, где ответы могут содержать много вложенных данных и поэтому хорошо сжимаются.
Профилирование GraphQL запросов — еще один важный аспект оптимизации. HotChocolate предлагает Apollo Tracing, который показывает время выполнения каждого резолвера:

C#
1
2
services.AddGraphQLServer()
  .AddApolloTracing(TracingPreference.Always);
Результаты трассировки можно увидеть в поле extensions ответа:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": { /* ... */ },
  "extensions": {
    "tracing": {
      "version": 1,
      "startTime": "2023-10-06T12:00:00Z",
      "endTime": "2023-10-06T12:00:00.123Z",
      "duration": 123000000,
      "execution": {
        "resolvers": [
          /* подробная информация о времени выполнения каждого резолвера */
        ]
      }
    }
  }
}
На практике я бы рекомендовал включать трассировку только в среде разработки или для отладки проблем с производительностью, так как она создаёт некоторые накладные расходы.

Для тестирования безопасности GraphQL API, помимо стандартных инструментов вроде OWASP ZAP, есть специализированные инструменты, например, InQL. Они позволяют осуществлять фаззинг и проверять API на типичные уязвимости, специфичные для GraphQL — от инъекций до DoS-атак через сложные запросы.

Не стоит забывать и об оптимизации самой базы данных. Даже самый эффективный DataLoader не спасёт, если таблицы не имеют нужных индексов. Анализируйте планы выполнения запросов и добавляйте индексы для часто используемых полей фильтрации и поиска:

C#
1
2
3
4
5
modelBuilder.Entity<Book>()
    .HasIndex(b => b.Title);
    
modelBuilder.Entity<Book>()
    .HasIndex(b => b.AuthorId);
Наконец, для по-настоящему высоконагруженных систем, рассмотрите возможность денормализации данных для популярных запросов. Например, можно хранить предварительно вычисленные данные для дашбордов, чтобы избежать сложных JOIN-операций во время выполнения запроса.

Безопасность и масштабирование GraphQL API



GraphQL имеет особенности, которые требуют специфического подхода к безопасности. Гибкость запросов – сильная сторона технологии – одновременно становится и её ахиллесовой пятой. Если вы не установите ограничения, злоумышленник может отправить запрос, который вызовет каскадную загрузку огромного объёма данных и эффективно положит ваш сервер.

Помимо уже рассмотренных мер по ограничению сложности запросов, стоит обратить внимание на защиту от инъекций. В отличие от классических SQL-инъекций, GraphQL-запросы могут содержать вредоносные переменные или директивы. Хотя HotChocolate автоматически защищает от большинства таких атак, следует осторожно обрабатывать пользовательский ввод, особенно когда он используется для динамического построения запросов:

C#
1
2
3
4
5
6
7
8
// Небезопасно! Никогда так не делайте:
var query = $"query {{ book(id: \"{userInput}\") {{ title }} }}";
var result = await executor.ExecuteAsync(query);
 
// Правильный способ - использование переменных:
var query = "query($id: ID!) { book(id: $id) { title } }";
var variables = new { id = userInput };
var result = await executor.ExecuteAsync(query, variables);
Ещё одна важная стратегия безопасности – правильная обработка ошибок. По умолчанию GraphQL возвращает подробную информацию об ошибках, что может раскрыть детали внутренней работы системы. Настройте кастомный обработчик ошибок, который будет скрывать чувствительную информацию от клиентов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SecureErrorFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
        if (error.Exception is AccessViolationException)
            return error.WithMessage("Доступ запрещён");
            
        if (error.Exception is Exception ex)
        {
            // Логируем исключение для отладки
            _logger.LogError(ex, "GraphQL error");
            // Возвращаем клиенту общее сообщение
            return error.WithMessage("Внутренняя ошибка сервера");
        }
        
        return error;
    }
}
Что касается масштабирования, GraphQL API часто становятся узким местом системы именно из-за своей гибкости. Один сложный запрос может потреблять значительные ресурсы, поэтому горизонтальное масштабирование – ключевая стратегия. Для эффективного горизонтального масштабирования важно обеспечить отсутствие состояния (statelessness) в вашем API. HotChocolate хорошо работает с несколькими экземплярами за балансировщиком нагрузки, если вы не используете локальное кэширование или состояние сессии.

Для действительно крупных систем рассмотрите федерацию GraphQL – подход, при котором несколько независимых GraphQL-серверов объединяются в единую схему. HotChocolate поддерживает это через механизм stitching:

C#
1
2
3
4
5
6
services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddRemoteSchema("products", ignoreRootTypes: true)
    .AddRemoteSchema("customers", ignoreRootTypes: true)
    .AddTypeExtensionsFromFile("./Stitching.graphql");
Файл stitching.graphql определяет, как связываются типы из разных схем:

JSON
1
2
3
4
extend type Order {
  customer: Customer @delegate(schema: "customers", path: "customer(id: $fields:customerId)")
  products: [Product] @delegate(schema: "products", path: "productsByIds(ids: $fields:productIds)")
}
Такой подход позволяет разделить большую монолитную схему на микросервисы, каждый из которых может масштабироваться независимо.

При масштабировании GraphQL API особое внимание уделите распределённому кэшированию. Локальный кэш на каждом узле может привести к несогласованным данным. Используйте распределённые решения вроде Redis:

C#
1
2
3
4
services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddRedisQueryStorage(_ => ConnectionMultiplexer.Connect("localhost:6379"));
Не менее важна и стратегия инвалидации кэша. В GraphQL сложно предсказать, какие запросы затрагивают какие данные, поэтому часто применяют тагирование кэша по типам сущностей:

C#
1
2
3
4
5
[UseCaching(cacheTags: new[] { "Book", "Author" })]
public Book GetBook(int id, [ScopedService] AppDbContext context)
{
    return context.Books.First(b => b.Id == id);
}
Затем, при изменении данных, инвалидируют соответствующие теги:

C#
1
2
3
4
5
[UseInvalidateCacheTags(Tags = new[] { "Book" })]
public async Task<Book> AddBook(AddBookInput input, [ScopedService] AppDbContext context)
{
    // Реализация добавления книги
}

Асинхронная коммуникация и микросервисы с GraphQL



Если вы строите современные распределённые системы, то рано или поздно столкнётесь с необходимостью организовать не только запросы и мутации, но и асинхронный поток данных. GraphQL отлично справляется с этой задачей благодаря механизму подписок (Subscriptions). Подписки — это долгоживущие запросы, которые устанавливают соединение между клиентом и сервером и позволяют серверу отправлять данные, когда происходят определённые события. В C# и HotChocolate для реализации подписок используются потоки (streams) и специальные резолверы:

C#
1
2
3
4
5
6
7
8
9
public class Subscription
{
    [Subscribe]
    public Book BookAdded([EventMessage] Book book) => book;
    
    [Subscribe(MessageType = typeof(string))]
    public Book BookTitleChanged([EventMessage] string title) => 
        new Book { Title = title };
}
Для отправки событий в поток используется сервис ITopicEventSender:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Mutation
{
    public async Task<Book> AddBook(
        AddBookInput input,
        [ScopedService] AppDbContext context,
        [Service] ITopicEventSender eventSender)
    {
        var book = new Book
        {
            Title = input.Title,
            // ... другие поля
        };
        
        context.Books.Add(book);
        await context.SaveChangesAsync();
        
        // Отправляем событие в поток подписки
        await eventSender.SendAsync(nameof(Subscription.BookAdded), book);
        
        return book;
    }
}
На стороне клиента подписка выглядит так:

JSON
1
2
3
4
5
6
7
8
9
subscription {
  bookAdded {
    id
    title
    author {
      name
    }
  }
}
Для транспорта сообщений в реальном времени HotChocolate использует WebSockets, но может быть настроен и на другие протоколы.

Интеграция с микросервисами — ещё один сценарий, где GraphQL демонстрирует свою мощь. Представим, что ваша система разделена на несколько сервисов: один управляет книгами, другой — авторами, третий — заказами. Традиционный подход предполагает создание API-гейтвея, который агрегирует данные. С GraphQL эта задача становится проще благодаря механизму ститчинга (schema stitching).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddQueryType(d => d.Name("Query"))
            .AddRemoteSchema("books", 
                ignoreRootTypes: true,
                includeTypeNames: true)
            .AddRemoteSchema("authors", 
                ignoreRootTypes: true,
                includeTypeNames: true)
            .AddTypeExtensionsFromFile("./stitching.graphql");
    }
}
Файл stitching.graphql связывает поля из разных схем:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extend type Query {
  topBooks: [Book] @delegate(schema: "books")
  popularAuthors: [Author] @delegate(schema: "authors")
}
 
extend type Book {
  author: Author 
    @delegate(schema: "authors", path: "author(id: $fields:authorId)")
}
 
extend type Author {
  books: [Book] 
    @delegate(schema: "books", path: "booksByAuthor(authorId: $source:id)")
}
В микросервисной архитектуре масштабируемость критична. GraphQL-федерация — подход Apollo, который HotChocolate поддерживает через механизм Hot Chocolate Gateway — позволяет создать распределённую схему, где каждый микросервис управляет своей частью схемы, но клиент видит её как единое целое.

Когда говорим об асинхронных взаимодействиях в микросервисах, стоит упомянуть интеграцию с брокерами сообщений. HotChocolate легко интегрируется с RabbitMQ, Kafka или Azure Service Bus для обработки событий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BookAddedEventConsumer : BackgroundService
{
    private readonly ITopicEventSender _eventSender;
    private readonly IModel _channel;
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _channel.QueueDeclare("book_events", durable: true, exclusive: false);
        var consumer = new EventingBasicConsumer(_channel);
        
        consumer.Received += (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            var book = JsonSerializer.Deserialize<Book>(message);
            
            // Отправляем событие в GraphQL-подписку
            _eventSender.SendAsync(nameof(Subscription.BookAdded), book);
        };
        
        _channel.BasicConsume(queue: "book_events", autoAck: true, consumer: consumer);
        
        // Ожидаем остановки службы
        await Task.Delay(-1, stoppingToken);
    }
}
Такой паттерн позволяет эффективно связывать микросервисы через события и предоставлять клиентам реал-тайм обновления через GraphQL-подписки.

Регистрация, операции Crud в asp web api
Здравствуйте! Пишу Backend для веб-приложения на asp web api. Есть рабочая регистрация и...

Напишите CRUD приложение с использованием Free REST API.
Выбрать из этих либо же взять любую другую: - https://reqres.in/ -...

GraphQL validation error
..... const app = Relay.createContainer(App, { fragments: { viewer: () =&gt; Relay.QL` ...

MeteorJS и GraphQL
Привет, Хочу сказать что до недавнего времени MeteorJS и GraphQL являются какойто мутью в...

Graphql n+1 во вложенных запросах, даже с prefetch_related
Всем привет, на проекте использую graphql из библиотеки graphene-django для django. И заметил такую...

Куда нужно устанавливать GraphQL?
GraphQL пр использовании нужно настраивать только на одной стороне, либо на двух? Допустим, есть...

Graphql "Field \"get_flights_multi\" argument \"requests\" of type \"[MultiCityParameters]!\" is required but not provid
Не могу понять в чем ошибка.

graphql mutation
Можете помочь составить мутацию? type Mutation { createCustomMatch(input:...

Как осуществить мутацию с Graphql
App.vue &lt;template&gt; &lt;div id=&quot;app&quot;&gt; &lt;div style=&quot;margin: 0 20% 0 20%&quot;&gt; &lt;form...

Отправка запроса GraphQL с использованием фреймворка Yew
Всем привет! У меня есть клиент на Yew и сервер с API GraphQL. Не нашел примеров, как...

GraphQL: подскажите по теории
Сел изучать GraphQL. На хабре приводят пример запроса: query { stuff { eggs shirt...

Вывести массив объектов из одного поля graphql
Здравствуйте. Делаю graphql api на golang Получаю информацию о пользователе запросом:...

Метки .net, api, c#, graphql
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Запустил конкурс "тем и промптов для текстовых квестов созданных почти чисто ИИ"
Adler 06.10.2026
Всем привет! За последние три-четыре дня я создал более 16 текстовых квестовых игр используя преимущественно по одному запросу к ИИ на игру. Мне так понравилось смотреть все ветки/ сцены во всех. . .
ИИ не может найти нужный язык в списке
Supersumestria 05.10.2026
Я ему даю вот такое изображение и прошу найти и подчеркнуть немецкий язык. Возвращает он вот это: https:/ / i. **********/ vqBWLe2. png Нужную строчку в 3й колонке просто выдумал. . Это. . .
Новая последняя моя музыка в SUNO
zorxor 05.10.2026
Здравствуйте, дорогие мои друзья! С большой радостью я хотел бы представить вам свою новую последнею музыку, которую сгенерировала мне по моей просьбе нейросеть SUNO. С уважением, zorxor. Это. . .
Nekobox - outbounds[0].transport: unknown transport type: raw
damix 01.10.2026
Фикс ошибки Правым кликом по серверу -> отладочная информация -> edit Заменить "net": "raw", на "net": "tcp", Нажать кнопку reload.
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
Мобильное приложение ColorStep
pavlinmavlin 17.09.2026
Реализовал приложение Красный, Зеленый, Синий в Unity3d + c#. Название изменил на ColorStep. Приложение прошло модерацию и теперь доступно для скачивания. Делал его сам, шаг за шагом — и вот,. . .
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru