Modified API returned data format to ditch use of GenericResponse wrapper

This commit is contained in:
Carl Tibule
2023-02-08 18:56:06 -06:00
parent 7e48e097c0
commit 0413abf587
11 changed files with 114 additions and 126 deletions

View File

@ -2,9 +2,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Net; using System.Net;
using YABA.API.ViewModels; using YABA.API.ViewModels.Bookmarks;
using YABA.API.ViewModels.Tags;
using YABA.Common.DTOs.Bookmarks; using YABA.Common.DTOs.Bookmarks;
using YABA.Common.DTOs.Tags;
using YABA.Service.Interfaces; using YABA.Service.Interfaces;
namespace YABA.API.Controllers namespace YABA.API.Controllers
@ -13,15 +13,17 @@ namespace YABA.API.Controllers
[Authorize, Route("api/v{version:apiVersion}/[controller]")] [Authorize, Route("api/v{version:apiVersion}/[controller]")]
public class BookmarksController : ControllerBase public class BookmarksController : ControllerBase
{ {
private readonly IMapper _mapper;
private readonly IBookmarkService _bookmarkService; private readonly IBookmarkService _bookmarkService;
public BookmarksController(IBookmarkService bookmarkService) public BookmarksController(IMapper mapper, IBookmarkService bookmarkService)
{ {
_mapper = mapper;
_bookmarkService = bookmarkService; _bookmarkService = bookmarkService;
} }
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(GenericResponse<CreateBookmarkRequestDTO>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateBookmarkRequestDTO request) public async Task<IActionResult> Create([FromBody] CreateBookmarkRequestDTO request)
{ {
@ -29,92 +31,94 @@ namespace YABA.API.Controllers
var result = await _bookmarkService.CreateBookmark(request); var result = await _bookmarkService.CreateBookmark(request);
if(!result.IsSuccessful) return BadRequest(new GenericResponse<CreateBookmarkRequestDTO>(result)); if(result == null) return BadRequest();
return Ok(new GenericResponse<CreateBookmarkRequestDTO>(result)); return CreatedAtAction(nameof(Create), _mapper.Map<BookmarkResponse>(result));
} }
[HttpPost("{id}/Tags")] [HttpPost("{id}/Tags")]
[ProducesResponseType(typeof(IEnumerable<GenericResponse<string>>),(int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<TagResponse>),(int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> UpdateBookmarkTags(int id, [FromBody] UpdateBookmarkTagRequest request) public async Task<IActionResult> UpdateBookmarkTags(int id, [FromBody] UpdateBookmarkTagRequest request)
{ {
if (request.Tags == null) return BadRequest(); if (request.Tags == null || !request.Tags.Any()) return BadRequest();
var result = await _bookmarkService.UpdateBookmarkTags(id, request.Tags); var result = await _bookmarkService.UpdateBookmarkTags(id, request.Tags);
if (result.All(x => !x.IsSuccessful)) return NotFound(); if (result == null) return NotFound();
return Ok(result.Select(x => new GenericResponse<string>(x))); return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
} }
[HttpPut("{id}")] [HttpPut("{id}")]
[ProducesResponseType(typeof(GenericResponse<UpdateBookmarkRequestDTO>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> UpdateBookmark(int id, [FromBody] UpdateBookmarkRequestDTO request) public async Task<IActionResult> UpdateBookmark(int id, [FromBody] UpdateBookmarkRequestDTO request)
{ {
// TODO: Add support for HTTP PATCH // TODO: Add support for HTTP PATCH
var result = await _bookmarkService.UpdateBookmark(id, request); var result = await _bookmarkService.UpdateBookmark(id, request);
if (!result.IsSuccessful) return NotFound(); if (result == null) return NotFound();
return Ok(new GenericResponse<UpdateBookmarkRequestDTO>(result)); return Ok(_mapper.Map<BookmarkResponse>(result));
} }
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(GenericResponse<IEnumerable<BookmarkDTO>>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<BookmarkResponse>), (int)HttpStatusCode.OK)]
public IActionResult GetAll() public IActionResult GetAll()
{ {
var result = _bookmarkService.GetAll(); var result = _bookmarkService.GetAll();
return Ok(new GenericResponse<IEnumerable<BookmarkDTO>>(result)); return Ok(_mapper.Map<IEnumerable<BookmarkResponse>>(result));
} }
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType(typeof(GenericResponse<BookmarkDTO>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Get(int id) public async Task<IActionResult> Get(int id)
{ {
var result = await _bookmarkService.Get(id); var result = await _bookmarkService.Get(id);
if (!result.IsSuccessful) return NotFound(); if (result == null) return NotFound();
return Ok(new GenericResponse<BookmarkDTO>(result)); return Ok(_mapper.Map<BookmarkResponse>(result));
} }
[HttpGet("{id}/Tags")] [HttpGet("{id}/Tags")]
[ProducesResponseType(typeof(GenericResponse<IEnumerable<TagSummaryDTO>>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public IActionResult GetBookmarkTags(int id) public IActionResult GetBookmarkTags(int id)
{ {
var result = _bookmarkService.GetBookmarkTags(id); var result = _bookmarkService.GetBookmarkTags(id);
if (!result.IsSuccessful) return NotFound(); if (result == null) return NotFound();
return Ok(new GenericResponse<IEnumerable<TagSummaryDTO>>(result)); return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(typeof(GenericResponse<int>), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Delete(int id) public async Task<IActionResult> Delete(int id)
{ {
var result = await _bookmarkService.DeleteBookmark(id); var result = await _bookmarkService.DeleteBookmark(id);
if (!result.IsSuccessful) return NotFound(); if (!result.HasValue) return NotFound();
return Ok(new GenericResponse<int>(result)); return NoContent();
} }
[HttpDelete] [HttpDelete]
[ProducesResponseType(typeof(IEnumerable<GenericResponse<int>>), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> DeleteBookmarks([FromBody] DeleteBookmarksRequest request) public async Task<IActionResult> DeleteBookmarks([FromBody] DeleteBookmarksRequest request)
{ {
if (request.Ids == null || !request.Ids.Any()) return BadRequest();
var result = await _bookmarkService.DeleteBookmarks(request.Ids); var result = await _bookmarkService.DeleteBookmarks(request.Ids);
if(result.All(x => !x.IsSuccessful)) return NotFound(); if(result == null) return NotFound();
return Ok(result.Select((x) => new GenericResponse<int>(x))); return NoContent();
} }
} }
} }

View File

@ -1,6 +1,9 @@
using AutoMapper; using AutoMapper;
using YABA.API.ViewModels; using YABA.API.ViewModels;
using YABA.API.ViewModels.Bookmarks;
using YABA.API.ViewModels.Tags;
using YABA.Common.DTOs; using YABA.Common.DTOs;
using YABA.Common.DTOs.Bookmarks;
using YABA.Common.DTOs.Tags; using YABA.Common.DTOs.Tags;
namespace YABA.API.Settings namespace YABA.API.Settings
@ -10,6 +13,8 @@ namespace YABA.API.Settings
public AutoMapperProfile() public AutoMapperProfile()
{ {
CreateMap<UserDTO, UserResponse>(); CreateMap<UserDTO, UserResponse>();
CreateMap<TagSummaryDTO, TagResponse>();
CreateMap<BookmarkDTO, BookmarkResponse>();
} }
} }
} }

View File

@ -0,0 +1,17 @@
using YABA.API.ViewModels.Tags;
namespace YABA.API.ViewModels.Bookmarks
{
public class BookmarkResponse
{
public int Id { get; set; }
public DateTimeOffset CreatedOn { get; set; }
public DateTimeOffset LastModified { get; set; }
public string Title { get; set; }
public string? Description { get; set; }
public string? Note { get; set; }
public bool IsHidden { get; set; }
public string Url { get; set; }
public IEnumerable<TagResponse> Tags { get; set; }
}
}

View File

@ -1,4 +1,4 @@
namespace YABA.API.ViewModels namespace YABA.API.ViewModels.Bookmarks
{ {
public class DeleteBookmarksRequest public class DeleteBookmarksRequest
{ {

View File

@ -1,4 +1,4 @@
namespace YABA.API.ViewModels namespace YABA.API.ViewModels.Bookmarks
{ {
public class UpdateBookmarkTagRequest public class UpdateBookmarkTagRequest
{ {

View File

@ -1,22 +0,0 @@
using YABA.Common.DTOs;
using YABA.Common.Extensions;
namespace YABA.API.ViewModels
{
public class GenericResponse<T>
{
public T Entry { get; set; }
public int StatusId { get; set; }
public string StatusName { get; set; }
public string StatusMessage { get; set; }
public GenericResponse(CrudResultDTO<T> value)
{
// TODO: Find a way to bring this to AutoMapper
Entry = value.Entry;
StatusId = (int)value.CrudResult;
StatusName = value.CrudResult.ToString();
StatusMessage = value.CrudResult.GetDisplayName();
}
}
}

View File

@ -0,0 +1,8 @@
namespace YABA.API.ViewModels.Tags
{
public class TagResponse
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -9,8 +9,6 @@ namespace YABA.Common.DTOs.Bookmarks
public string? Description { get; set; } public string? Description { get; set; }
public string? Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public string? Url { get; set; }
[Required]
public string Url { get; set; }
} }
} }

View File

@ -1,12 +0,0 @@
using YABA.Common.Extensions;
using YABA.Common.Lookups;
namespace YABA.Common.DTOs
{
public class CrudResultDTO<T>
{
public CrudResultLookup CrudResult { get; set; }
public T Entry { get; set; }
public bool IsSuccessful => CrudResult.IsCrudResultSuccessful();
}
}

View File

@ -39,7 +39,7 @@ namespace YABA.Service
_mapper = mapper; _mapper = mapper;
} }
public CrudResultDTO<IEnumerable<BookmarkDTO>> GetAll() public IEnumerable<BookmarkDTO> GetAll()
{ {
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
@ -70,60 +70,52 @@ namespace YABA.Service
userBookmarkDTOs.Add(bookmarkDTO); userBookmarkDTOs.Add(bookmarkDTO);
} }
return new CrudResultDTO<IEnumerable<BookmarkDTO>> { Entry = userBookmarkDTOs, CrudResult = CrudResultLookup.RetrieveSuccessful }; return userBookmarkDTOs;
} }
public async Task<CrudResultDTO<CreateBookmarkRequestDTO>?> CreateBookmark(CreateBookmarkRequestDTO request) public async Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request)
{ {
var crudResult = new CrudResultDTO<CreateBookmarkRequestDTO>() { Entry = request, CrudResult = CrudResultLookup.CreateFailed };
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
if (!_roContext.Users.UserExists(currentUserId)) return crudResult; if (!_roContext.Users.UserExists(currentUserId)
|| await _roContext.Bookmarks.AnyAsync(x => x.UserId == currentUserId && x.Url == request.Url)) return null;
if(await _roContext.Bookmarks.AnyAsync(x => x.UserId == currentUserId && x.Url == request.Url))
{
crudResult.CrudResult = CrudResultLookup.CreateFailedEntryExists;
return crudResult;
}
var bookmark = _mapper.Map<Bookmark>(request); var bookmark = _mapper.Map<Bookmark>(request);
UpdateBookmarkWithMetaData(bookmark); UpdateBookmarkWithMetaData(bookmark);
bookmark.UserId = currentUserId; bookmark.UserId = currentUserId;
await _context.Bookmarks.AddAsync(bookmark); var newEntity = await _context.Bookmarks.AddAsync(bookmark);
if (await _context.SaveChangesAsync() > 0) crudResult.CrudResult = CrudResultLookup.CreateSucceeded; if (await _context.SaveChangesAsync() > 0) return _mapper.Map<BookmarkDTO>(newEntity.Entity);
return crudResult; return null;
} }
public async Task<CrudResultDTO<UpdateBookmarkRequestDTO>> UpdateBookmark(int id, UpdateBookmarkRequestDTO request) public async Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO request)
{ {
var crudResult = new CrudResultDTO<UpdateBookmarkRequestDTO>() { Entry = request, CrudResult = CrudResultLookup.UpdateFailed };
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
var bookmark = _context.Bookmarks.FirstOrDefault(x => x.UserId == currentUserId && x.Id == id); var bookmark = _context.Bookmarks.FirstOrDefault(x => x.UserId == currentUserId && x.Id == id);
if(bookmark == null) return crudResult; if(bookmark == null) return null;
bookmark.Title = request.Title; bookmark.Title = !string.IsNullOrEmpty(request.Title) ? request.Title : bookmark.Title;
bookmark.Description = request.Description; bookmark.Description = !string.IsNullOrEmpty(request.Description) ? request.Description : bookmark.Description;
bookmark.Note = request.Note; bookmark.Note = !string.IsNullOrEmpty(request.Note) ? request.Note : bookmark.Note;
bookmark.IsHidden = request.IsHidden; bookmark.IsHidden = request.IsHidden;
bookmark.Url = request.Url; bookmark.Url = !string.IsNullOrEmpty(request.Url) ? request.Url : bookmark.Url;
UpdateBookmarkWithMetaData(bookmark); UpdateBookmarkWithMetaData(bookmark);
if (await _context.SaveChangesAsync() > 0) crudResult.CrudResult = CrudResultLookup.UpdateSucceeded; if (await _context.SaveChangesAsync() > 0) return _mapper.Map<BookmarkDTO>(bookmark);
return crudResult; return null;
} }
public async Task<IEnumerable<CrudResultDTO<string>>> UpdateBookmarkTags(int id, IEnumerable<string> tags) public async Task<IEnumerable<TagSummaryDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
{ {
var crudResults = tags.Select((x) => new CrudResultDTO<string> { Entry = x, CrudResult = CrudResultLookup.UpdateFailed }).ToList();
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == currentUserId)) return crudResults; if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == currentUserId)) return null;
// Add tags that are not yet in the database // Add tags that are not yet in the database
var savedUserTags = _context.Tags.Where(x => x.UserId == currentUserId).ToList(); var savedUserTags = _context.Tags.Where(x => x.UserId == currentUserId).ToList();
@ -148,17 +140,25 @@ namespace YABA.Service
_context.BookmarkTags.RemoveRange(bookmarkTagsToRemove); _context.BookmarkTags.RemoveRange(bookmarkTagsToRemove);
await _context.BookmarkTags.AddRangeAsync(bookmarkTagsToAdd); await _context.BookmarkTags.AddRangeAsync(bookmarkTagsToAdd);
if (await _context.SaveChangesAsync() >= 0) crudResults.ForEach(x => x.CrudResult = CrudResultLookup.UpdateSucceeded); if (await _context.SaveChangesAsync() >= 0)
{
var updatedBookmarkTags = _roContext.BookmarkTags
.Include(x => x.Tag)
.Where(x => x.BookmarkId == id)
.Select(x => x.Tag);
return crudResults; return _mapper.Map<IEnumerable<TagSummaryDTO>>(updatedBookmarkTags);
}
return null;
} }
public async Task<CrudResultDTO<BookmarkDTO>> Get(int id) public async Task<BookmarkDTO?> Get(int id)
{ {
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId); int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
var bookmark = await _roContext.Bookmarks.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId); var bookmark = await _roContext.Bookmarks.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId);
if (bookmark == null) return new CrudResultDTO<BookmarkDTO> { CrudResult = CrudResultLookup.RetrieveFailed, Entry = null }; if (bookmark == null) return null;
var bookmarkTags = _roContext.BookmarkTags var bookmarkTags = _roContext.BookmarkTags
.Include(x => x.Tag) .Include(x => x.Tag)
@ -169,13 +169,13 @@ namespace YABA.Service
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark); var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
bookmarkDTO.Tags = _mapper.Map<IList<TagSummaryDTO>>(bookmarkTags); bookmarkDTO.Tags = _mapper.Map<IList<TagSummaryDTO>>(bookmarkTags);
return new CrudResultDTO<BookmarkDTO> { CrudResult = CrudResultLookup.RetrieveSuccessful, Entry = bookmarkDTO }; return bookmarkDTO;
} }
public CrudResultDTO<IEnumerable<TagSummaryDTO>> GetBookmarkTags(int id) public IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id)
{ {
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId); int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return new CrudResultDTO<IEnumerable<TagSummaryDTO>> { Entry = null, CrudResult = CrudResultLookup.RetrieveFailed }; if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return null;
var bookmarkTags = _roContext.BookmarkTags var bookmarkTags = _roContext.BookmarkTags
.Include(x => x.Tag) .Include(x => x.Tag)
@ -183,38 +183,28 @@ namespace YABA.Service
.Select(x => x.Tag) .Select(x => x.Tag)
.ToList(); .ToList();
var bookmarkTagDTOs = _mapper.Map<IEnumerable<TagSummaryDTO>>(bookmarkTags); return _mapper.Map<IEnumerable<TagSummaryDTO>>(bookmarkTags);
return new CrudResultDTO<IEnumerable<TagSummaryDTO>> { Entry = bookmarkTagDTOs, CrudResult = CrudResultLookup.RetrieveSuccessful };
} }
public async Task<CrudResultDTO<int>> DeleteBookmark(int id) public async Task<int?> DeleteBookmark(int id)
{ {
var crudResults = await DeleteBookmarks(new List<int> { id }); var result = await DeleteBookmarks(new List<int> { id });
return crudResults.FirstOrDefault(); return result.FirstOrDefault();
} }
public async Task<IEnumerable<CrudResultDTO<int>>> DeleteBookmarks(IEnumerable<int> ids) public async Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids)
{ {
var crudResults = ids.Select(x => new CrudResultDTO<int> { Entry = x, CrudResult = CrudResultLookup.DeleteFailed }).ToList();
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return crudResults; if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
var entriesToDelete = _context.Bookmarks.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList(); var entriesToDelete = _context.Bookmarks.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
var entryIdsToDelete = entriesToDelete.Select(x => x.Id); var entryIdsToDelete = entriesToDelete.Select(x => x.Id);
_context.Bookmarks.RemoveRange(entriesToDelete); _context.Bookmarks.RemoveRange(entriesToDelete);
if (await _context.SaveChangesAsync() <= 0) return crudResults; if (await _context.SaveChangesAsync() <= 0) return null;
// Update crudResults that were found in the entriesToDelete to success return ids;
foreach(var crudResult in crudResults)
{
if (entryIdsToDelete.Contains(crudResult.Entry))
crudResult.CrudResult = CrudResultLookup.DeleteSucceeded;
}
return crudResults;
} }
private int GetCurrentUserId() private int GetCurrentUserId()

View File

@ -8,14 +8,14 @@ namespace YABA.Service.Interfaces
{ {
public interface IBookmarkService public interface IBookmarkService
{ {
Task<CrudResultDTO<CreateBookmarkRequestDTO>> CreateBookmark(CreateBookmarkRequestDTO request); Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request);
Task<CrudResultDTO<UpdateBookmarkRequestDTO>> UpdateBookmark(int id, UpdateBookmarkRequestDTO request); Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO request);
Task<IEnumerable<CrudResultDTO<string>>> UpdateBookmarkTags(int id, IEnumerable<string> tags); Task<IEnumerable<TagSummaryDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags);
CrudResultDTO<IEnumerable<BookmarkDTO>> GetAll(); IEnumerable<BookmarkDTO> GetAll();
Task<CrudResultDTO<BookmarkDTO>> Get(int id); Task<BookmarkDTO?> Get(int id);
CrudResultDTO<IEnumerable<TagSummaryDTO>> GetBookmarkTags(int id); IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id);
Task<CrudResultDTO<int>> DeleteBookmark(int id); Task<int?> DeleteBookmark(int id);
Task<IEnumerable<CrudResultDTO<int>>> DeleteBookmarks(IEnumerable<int> ids); Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids);
} }
} }