При работе над созданием мощного API вы обнаружите, что имеете дело с ресурсами и другими ресурсами, и эти «ресурсы» имеют «отношения» между собой, и это мощно, поскольку мы можем использовать силу реляционных данных для создания удивительных способов отображения информации или использования. это. Но если вы на самом деле не понимаете, как определить эту связь, как я показал вам в своей последней статье об отношениях в Entity Framework Core, или вы не знаете, как заставить их работать вместе, как вы увидите позже в этом посте, тогда это может быть кошмаром для работы с первичными ключами, ограничениями, LINQ и т. д. 😰
По этой причине необходимо знать, как выставлять и представлять связанные объекты из ваших ресурсов веб-API. В этой статье вы узнаете, как этого добиться, создавая значимые конечные точки для получения необходимых ресурсов.
Что такое связанные сущности❓
Связанные объекты относятся к взаимосвязанным объектам или объектам в модели данных. В контексте баз данных и ORM (объектно-реляционное сопоставление), таких как Entity Framework Core, связанные сущности представляют ассоциации и отношения между различными таблицами или сущностями в схеме базы данных.
Эти отношения бывают разных видов, например, один-к-одному, один-ко-многим или многие-ко-многим. Они в основном говорят нам, как части соединяются вместе. Например, в приложении блога у сообщения может быть один автор (один к одному) или у автора может быть несколько сообщений (один ко многим). Эти отношения придают структуру и смысл нашим данным.
Понимание этих отношений похоже на раскрытие секретов вашей модели данных. Он позволяет создавать более сложные и взаимосвязанные модели, предоставляя вам возможность легко извлекать связанные данные и манипулировать ими. Вы можете легко получать доступ к информации из разных сущностей, выполнять запросы и создавать мощные приложения.
Подводя итог, связанные сущности — это связи между фрагментами данных в вашей базе данных. Они придают вашей модели данных ее структуру и определяют, как различные части работают вместе. Хорошо разобравшись в этих взаимосвязях, вы сможете перемещаться по своим данным, создавать эффективные запросы и создавать потрясающие приложения.
Как выставить связанные сущности 🔎
Предоставление связанных сущностей в Core Web API ASP.NET включает в себя обеспечение доступа к этим соединениям и возможности их извлечения для клиентов, которые взаимодействуют с вашим API. Предоставляя доступ к связанным сущностям, вы позволяете клиентам получать доступ к взаимосвязанным данным и манипулировать ими осмысленным образом.
Для этого можно использовать несколько подходов:
- Вложенная сериализация. Одним из способов является включение связанных сущностей в виде вложенных объектов в ответ JSON. Например, при получении сообщения в блоге вы также можете включить информацию об авторе как часть ответа, упрощая клиентам доступ как к сообщению, так и к его автору в одном запросе.
- Расширяемые конечные точки. Другой подход заключается в предоставлении расширяемых конечных точек, где клиенты могут указать, какие связанные объекты они хотят включить в ответ. Это дает клиентам больший контроль над данными, которые они получают, уменьшая ненужную передачу данных и повышая производительность.
- Ссылки на гипермедиа. API на основе гипермедиа используют ссылки для представления отношений между объектами. Включая ссылки в ваши ответы API, клиенты могут перемещаться по связанным объектам, следуя этим ссылкам. Этот подход обеспечивает более динамичный и гибкий способ представления связанных сущностей.
- Пользовательские конечные точки. В зависимости от требований вашего приложения вы можете создавать настраиваемые конечные точки, специально предназначенные для извлечения связанных сущностей. Например, у вас может быть конечная точка, которая возвращает все комментарии, связанные с записью в блоге, или все продукты в определенной категории.
При предоставлении связанных сущностей важно учитывать такие факторы, как производительность, размер данных и безопасность. Вы хотите найти баланс между предоставлением достаточного количества связанных данных для работы клиентов и предотвращением избыточной передачи данных, которая может повлиять на производительность.
В этой статье мы закончим тем, что будем использовать сочетание этих подходов, чтобы вернуть клиенту все необходимые ресурсы.
Получите код с GitHub 🐙
Как всегда, прежде чем мы перейдем к коду, если вы хотите следовать этому руководству, убедитесь, что вы получаете код из репозитория GitHub, и дважды проверьте, что вы получаете код из ветки «EfCoreRelationship».
Обновление PostRepository
класса ⤴️
Класс PostRepository
извлекает сообщения из базы данных, но ему не хватает мощности, поскольку он не возвращает автора для каждого репозитория, а только authorId
, и не возвращает tags
, который есть у Post
, поэтому мы расширим функциональность репозитория. класс, обновляющий свои методы и добавляющий два новых метода.
IRepository
класс обновлен ⬆️
public interface IPostRepository { Task<IEnumerable<Post>> GetPostAsync(QueryParameters parameters); Task<Post> GetPostAsync(int id); Task<IEnumerable<Post>> GetPostByAuthorId(int id); Task<IEnumerable<Post>> GetPostByTagId(int id); Task AddAsync(Post post); Task EditAsync(Post post); Task DeleteAsync(int id); }
Как видите, есть два новых метода GetPostByAuthorId
и GetPostByTagId
, которые предназначены для извлечения всех сообщений от пользователя и всех сообщений, связанных с определенным тегом, что является обычной функцией фильтрации любого существующего блога.
PostRepository
Класс 📫
public async Task<IEnumerable<Post>> GetPostAsync(QueryParameters parameters) { //Code ommited for brevity var pagedPosts = await allPosts .Skip((parameters.PageNumber - 1) * parameters.PageSize) .Take(parameters.PageSize) .Include(x => x.Author) .Include(x => x.Tags) .ToListAsync(); return pagedPosts; } public async Task<Post> GetPostAsync(int id) { var post = await context.Posts.AsNoTracking() .Where(x => x.Id == id) .Include(x => x.Author) .Include(x => x.Tags) .FirstOrDefaultAsync(); return post; } public async Task<IEnumerable<Post>> GetPostByAuthorId(int id) { var posts = await context.Posts.AsNoTracking() .Where(x => x.AuthorId == id) .Include(x => x.Author) .Include(x => x.Tags) .ToListAsync(); return posts; } public async Task<IEnumerable<Post>> GetPostByTagId(int id) { var posts = await context.Posts.AsNoTracking() .Include(x => x.Author) .Include(x => x.Tags) .Where(x => x.Tags.Any(t => t.Id == id)) .ToListAsync(); return posts; }
Первый метод — это GetPostAsync
, который мы создали для получения ответа с разбивкой на страницы для всех сообщений. Здесь ваша работа заключается в том, чтобы включить автора сообщения и соответствующие теги для каждого сообщения. Таким образом, в финальном запросе перед вызовом ToListAsync
вы добавите два вызова метода Include
, один для автора, а второй для тегов, таким образом, ваш ответ на сообщение теперь будет включать эти наборы данных.
Следующий метод — это метод GetPostByAuthorId
, который возвращает все сообщения, принадлежащие одному автору. Сначала вы фильтруете сообщение от автора с соответствующим идентификатором, а затем снова вызываете два метода Include
, чтобы добавить данные об авторе и теге в ответ.
Наконец, GetPostByTagId
похож на GetPostByAuthorId
, но вы заметите, что сначала есть два вызова метода Include
, а затем фильтр, использующий where
для фильтрации по указанному Tag
. Это должно быть сделано таким образом, потому что отношение Post
⇒ Tag
является отношением «многие ко многим», поэтому нет ни одного тега для фильтрации, как в случае с Author
, а есть набор тегов, и нам нужно выбрать тот, который мы хотим отфильтровать. для.
Предвидение цикла самореференции из-за ошибки свойства ➿
Теперь вы, вероятно, пойдете к контроллеру Post
и обновите его, чтобы проверить новые изменения, верно? Что ж, это не так просто, потому что, если вы пойдете и действительно сделаете это, вы получите ошибку ссылки на себя, потому что, когда вы попытаетесь получить список сообщений, вы получите с ним автора, а затем вы снова получите сообщения и вы получите его автора, и вы как бы видите, к чему все идет? Да, это бесконечный цикл, и вы знаете, что программирование и бесконечные циклы в большинстве случаев несовместимы, поэтому, чтобы избежать этого, вам придется использовать Dtos для возврата ваших ресурсов, потому что вы не должны возвращать свои модели данных как они есть, всегда рекомендуется создавать некоторые Dtos, чтобы возвращать только те данные, которые мы хотим показать, и избегать такого рода проблем, и второе, что вы должны сделать, чтобы избежать бесконечного цикла в вашем ответе, это добавить некоторые конфигурации в NewtonsoftJson
.AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
Вышеупомянутая строка кода предотвратит повторение цикла ссылок снова и снова, игнорируя запрос на постоянный возврат вложенного JSON.
Добавление новых Dtos 🏭
public record AuthorOnlyResponseDto(int Id, string Name); public record TagOnlyResponseDto(int Id, string Name, string Description);
Как было сказано ранее, вам нужно будет использовать Dtos для возврата ресурсов, в этом случае мы возвращаем автора без коллекции Post
и Tag
без коллекции Post
.
Обновление PostResponseDto
📬
public record PostResponseDTO( [property: JsonPropertyOrder(1)] int Id, [property: JsonPropertyOrder(2)] string Title, [property: JsonPropertyOrder(3)] string Body, [property: JsonPropertyOrder(4)] DateTime CreatedDate, [property: JsonPropertyOrder(5)] AuthorOnlyResponseDto Author, [property: JsonPropertyOrder(6)] ICollection<TagOnlyResponseDto> Tags);
В этом Dto происходят некоторые вещи. Во-первых, мы добавляем атрибут JsonPropertyOrder
, чтобы установить порядок, в котором каждое свойство будет возвращено, чтобы иметь красиво отформатированный ответ, потому что в противном случае свойства упорядочены в алфавитном порядке для Author
, теперь мы возвращаем AuthorOnlyResponse
, поэтому мы просто возвращаем ресурс автора без вложенных Post
и, наконец, мы возвращаем коллекцию TagOnlyResponseDto
, чтобы вернуть только коллекцию Tag
без вложенных Post
.
Добавление новых картографических профилей 🗺️
Теперь, чтобы ваши Dtos работали, добавьте соответствующие сопоставления в их классы профилей, как показано ниже.
public AuthorProfiles() { CreateMap<Author, AuthorOnlyResponseDto>(); } public TagProfiles() { CreateMap<Tag, TagOnlyResponseDto>(); }
Обновление AuthorsController
✍🏾
После изменений в классе PostRepository
нет необходимости обновлять PostsController
, так как все, что нам было нужно в контроллере, это вернуть автора и теги, связанные с публикацией, и это делается в классе репозитория, поэтому теперь вам нужно обновить Authors
и Tags
контроллеров добавить конечную точку для возврата всех постов под определенным автором и то же самое для тегов.
[ApiController] [Route("api/authors")] public class AuthorsController : ControllerBase { private readonly IAuthorRepository repository; private readonly IPostRepository postRepository; private readonly IMapper mapper; public AuthorsController(IAuthorRepository repository, IPostRepository postRepository, IMapper mapper) { this.repository = repository; this.postRepository = postRepository; this.mapper = mapper; } [HttpGet] public async Task<IActionResult> GetAuthor() { var authors = await repository.GetAsync(); var authorsDto = mapper.Map<IEnumerable<AuthorOnlyResponseDto>>(authors); return Ok(authorsDto); } [HttpGet("{id:int}")] public async Task<IActionResult> GetAuthor(int id) { var author = await repository.GetAsync(id); var authorDto = mapper.Map<AuthorOnlyResponseDto>(author); return Ok(authorDto); } [HttpGet("{id:int}/posts")] public async Task<IActionResult> GetPostByAuthorId(int id) { var posts = await postRepository.GetPostByAuthorId(id); var postDto = mapper.Map<IEnumerable<PostResponseDTO>>(posts); return Ok(postDto); } }
Вам нужно будет добавить ссылку на интерфейс IPostRepository
, чтобы получить коллекцию постов от автора. Затем в обоих методах Get вам нужно только сопоставить ответ от Author
с AuthorOnlyResponseDto
, чтобы вернуть только ресурс автора, а не его вложенные ресурсы.
Наконец, вам нужно будет добавить новые имена конечных точек GetPostByAuthorId
, которые будут получать все сообщения от определенного автора. Во-первых, вы должны указать маршрут как {id:int}/posts
, чтобы способ вызова этой конечной точки был таким, как этот api/authors/{authoId}/posts
, и это даст нам все сообщения от указанного автора. Код очень похож на оба предыдущих метода get, но вместо того, чтобы использовать репозиторий автора здесь, вам нужно будет вызвать метод postRepository.GetPostByAuthorId
и после этого преобразовать ответ в PostResponseDto
.
Обновление TagsController
🏷️
[ApiController] [Route("api/tags")] public class TagsController : ControllerBase { private readonly ITagRepository repository; private readonly IPostRepository postRepository; private readonly IMapper mapper; public TagsController(ITagRepository repository, IPostRepository postRepository ,IMapper mapper) { this.repository = repository; this.postRepository = postRepository; this.mapper = mapper; } [HttpGet] public async Task<IActionResult> GetTag() { var tags = await repository.GetAsync(); var tagsDto = mapper.Map<IEnumerable<TagOnlyResponseDto>>(tags); return Ok(tagsDto); } [HttpGet("{id:int}")] public async Task<IActionResult> GetTag(int id) { var tag = await repository.GetAsync(id); var tagDto = mapper.Map<TagOnlyResponseDto>(tag); return Ok(tagDto); } [HttpGet("{id:int}/posts")] public async Task<IActionResult> GetPostFromTagId(int id) { var posts = await postRepository.GetPostByTagId(id); var postsDto = mapper.Map<IEnumerable<PostResponseDTO>>(posts); return Ok(postsDto); } }
Для TagsController
все почти то же самое, вам нужно внедрить интерфейс IPostRepository
, а затем обновить методы get, отображающие ответ на TagOnlyResponseDto
, и, наконец, добавить новую конечную точку, чтобы получить все Posts
под определенным Tag
.
Тестирование API 🧪
Теперь давайте приступим к тестированию новых конечных точек и проверим наши обновленные ответы.
Получение всех авторов ✍🏾
[ { "id": 4, "name": "John Doe" }, { "id": 5, "name": "Oscar Montenegro" }, { "id": 6, "name": "Yolanda Montenegro" } ]
В контроллере авторов мы получаем только идентификатор и имя автора, раскрывая только те данные, которые мы ищем, не раскрывая вложенную коллекцию сообщений для каждого автора, для чего мы будем использовать новую конечную точку.
Получение всех Posts
от Author
✍🏾
[ { "id": 11, "title": "Setting up a CI/CD Pipeline", "body": "Setup a CI/CD Pipeline using GitHub actions", "createdDate": "2023-05-29T13:25:52.0306732", "author": { "id": 5, "name": "Oscar Montenegro" }, "tags": [ { "id": 2, "name": "Programming", "description": "Topics related to programming" } ] }, { "id": 13, "title": "GitHub basics", "body": "Check out this amazing series on the GitHub basics to start working on a code repository", "createdDate": "2023-05-30T14:25:48.3537618", "author": { "id": 5, "name": "Oscar Montenegro" }, "tags": [ { "id": 1, "name": "My first Tag", "description": "This is the first Tag" }, { "id": 2, "name": "Programming", "description": "Topics related to programming" }, { "id": 4, "name": "DevOps", "description": "Learn the latest DevOps trends and news" } ] } ]
Здесь мы получаем все сообщения для автора с идентификатором 5, и что имеет смысл, когда вы получаете сообщение, так это получение автора из этого сообщения, а также связанных тегов для этого сообщения. По этой причине это оптимальный ответ публикации, раскрывающий связанные и релевантные сущности. Ответ хорошо отформатирован, представляя свойства поста, а затем раскрывая автора и, наконец, массив тегов.
Получение всех тегов 🏷️
[ { "id": 1, "name": "My first Tag", "description": "This is the first Tag" }, { "id": 2, "name": "Programming", "description": "Topics related to programming" }, { "id": 4, "name": "DevOps", "description": "Learn the latest DevOps trends and news" } ]
Теперь теги возвращают только те свойства, которые мы хотим предоставить для этой конечной точки, а именно name
и description
, а набор сообщений, связанных с каждым тегом, можно получить в отдельной конечной точке.
Получение всех сообщений, связанных с тегом 🏷️
[ { "id": 11, "title": "Setting up a CI/CD Pipeline", "body": "Setup a CI/CD Pipeline using GitHub actions", "createdDate": "2023-05-29T13:25:52.0306732", "author": { "id": 5, "name": "Oscar Montenegro" }, "tags": [ { "id": 2, "name": "Programming", "description": "Topics related to programming" } ] }, { "id": 13, "title": "GitHub basics", "body": "Check out this amazing series on the GitHub basics to start working on a code repository", "createdDate": "2023-05-30T14:25:48.3537618", "author": { "id": 5, "name": "Oscar Montenegro" }, "tags": [ { "id": 1, "name": "My first Tag", "description": "This is the first Tag" }, { "id": 2, "name": "Programming", "description": "Topics related to programming" }, { "id": 4, "name": "DevOps", "description": "Learn the latest DevOps trends and news" } ] } ]
Этот ответ создается после вызова конечной точки api/tags/2/posts
, которая возвращает все сообщения, связанные с тегом programming
, и, как и прежде, вы можете видеть, что ответ содержит свойства сообщения вместе с автором сообщения и соответствующими тегами. .
Контролер сообщений 🏤
Ответ в контроллере сообщений будет выглядеть так же, как и раньше, поэтому я не буду публиковать ответ, чтобы избавить вас от чтения того же самого 😅, но важно еще раз сказать, что мы отформатировали ответ на сообщение именно так, как мы хотели, и в способ, который имеет смысл, поэтому всякий раз, когда клиент ищет сообщение или набор сообщений, они содержат все необходимые данные для отображения сообщения с именем автора и набором тегов для этого сообщения.
Вывод 🌇
Подводя итог, можно сказать, что предоставление связанных сущностей в вашем веб-API открывает целый мир возможностей для работы со взаимосвязанными данными. Делая эти отношения доступными для клиентов, вы даете им возможность легко извлекать, изменять и перемещаться по связанным данным.
Крайне важно найти баланс между предоставлением достаточного количества связанных данных и оптимизацией производительности при разработке конечных точек API и ответов. Необходимо учитывать такие факторы, как размер данных, влияние на производительность и требования безопасности, чтобы обеспечить бесперебойную и эффективную работу для пользователей API.
Продолжая разрабатывать свой веб-API, используйте стратегии и методы, описанные в этой статье, для эффективного раскрытия связанных сущностей. Не забудьте согласовать свой подход с конкретными требованиями и целями вашего приложения.
Вооружившись знаниями и пониманием того, как предоставлять доступ к связанным сущностям, вы сможете вывести свой основной веб-API ASP.NET на новый уровень. Применяя правильный подход, вы создадите API, который предлагает плавную навигацию, надежное извлечение данных и эффективное управление взаимосвязанными данными.
Мы достигли 3K просмотров на Unit Coding 🥳🎉🎊
Я так рад поделиться, что в прошлые выходные после публикации моей статьи о взаимосвязях Entity framework я ожидал достичь 1K просмотров в месяц, но внезапно, когда я проснулся в субботу, я проверил, и только за одну ночь было более 2K просмотров! Чувак, я был так счастлив, и сегодня мы преодолели отметку в 3 тысячи просмотров. Я хочу поблагодарить всех вас, ребята, которые проверяли мой контент и постоянно читали, без вас это было бы невозможно, так что это достижение для всех нас, кто является частью этого сообщества.
Также хочу поделиться с вами, что в этой серии есть где-то 10 постов, прежде чем она подойдет к концу, и я уже готовлюсь ко всем новым сериям, которые я хочу запустить, например, серию по разработке веб-приложений с помощью Blazor, ASP NET. MVC, LINQ и многое другое, и после завершения этой серии статей о разработке веб-API я объявлю о выпуске сюрприза для всех вас, которые поддерживали меня на протяжении всего этого удивительного пути становления писателем. вы не теряете его.
В моей голове так много слов, которые я не могу выразить, и так много всего произошло с самого начала этого путешествия. Я уже третий месяц без работы, это было тяжело, и у меня почти заканчиваются деньги (без шуток 😨), но я знаю, что у меня есть варианты, и есть кое-что, что я могу сделать до того, как это произойдет. Ну, я думаю, что я должен пока попрощаться, потому что я делаю это дольше, чем должно быть 😂 спасибо за вашу добрую поддержку и красивые слова, не забудьте подписаться на меня в моем блоге Unit Coding и на моем канале YouTube. » под тем же именем, и если вам нравится Твиттер, вы можете найти меня там как @OscarOsempu. Я все еще стараюсь постоянно твитить, но иногда это сложно. Спасибо и до встречи в следующей главе! Удачного кодирования! 👋🏾