1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

予約サービスのAPIを構築してみる

Posted at

現在レシートローラーでは様々なアドオンサービスを構築しています。まだリリースとはいきませんが、現在検討している予約サービスのAPIを構築してみたいと思います。いろいろお店や教室で利用いただけるように、サービス化していきたいとは思いますが、まずは既存サービスのサポートを受けるための予約サービスを作ってみます。最終的には他サービスにも連携するような予約APIを想定しています。

作るものをざっくり絵にする

先ずは絵にしてみます。この段階では詳細は決まってなくても大丈夫です(持論)。

ざっくりとこんな感じのサービスを作ります。

image.png

管理者、サービス、クライアントサーバー、利用者の4人がいる想定です。

  1. まず、管理者がカレンダーや予約可能な商品を作成します。 
  2. 利用者はカレンダーを選択し、予約可能な日付をクライアントサーバーへリクエストします。
  3. クライアントサーバーはサービスに同じく対象カレンダーの予約可能な日付をリクエストします。
  4. サービスからの値を加工して利用者に予約可能な日付を表示します。
  5. 利用者は予約をしたいスロットを選択し、クライアント経由でサービスに予約をいれます。
  6. 予約の確認メールまたは何かしらのお知らせを利用者に送ります、同じく管理者にも送ります。

というのが、流れと範囲です。

データーの構成としては、ここもざっくりこんな感じです。

image.png

アカウントがあり、組織というグループ要素があり、その中にカレンダーが複数あり、カレンダーの中に予約が複数あるようなイメージです。

ここでいうカレンダーは会議室だったり、飲食店のテーブルだったり、ヨガ教室の部屋だったりになります。各要素ごとに細かな設定項目などはあるかと思いますが、一旦ざっくり構成だけ書いてみます。

データの定義を書く

作りたいモノの絵をかいたところで、次は具体的に書くデータのモデルを書き起こします。今回はAzure SQLを使って進めます。

ユーザーと組織を定義

一番大きな枠で、アカウントと組織がありますので、まずはこの2つのモデルから。アカウントに関してはASP.NET Core Identityを最終的には継承します。 ここでいうユーザーは管理者と予約をする利用者の両方になります。利用者と管理者の区別はroleでつけるものとします。

 // Represents a user with an optional name, extending the IdentityUser class with additional properties.
 public class UserModel : IdentityUser
 {
     public string? Name { get; set; }  // Optional name of the user.
 }

 // Defines the membership details for an organization including roles and user associations.
 [Table("OrganizationMemberships")]
 public class OrganizationMembershipModel
 {
     // Constructor to initialize an OrganizationMembership instance.
     public OrganizationMembershipModel(string id, string organizationId, string userId, string roleId)
     {
         Id = id;
         OrganizationId = organizationId;
         UserId = userId;
         RoleId = roleId;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the membership.
     public string OrganizationId { get; set; }  // Associated organization identifier.
     public string UserId { get; set; }  // Associated user identifier.
     public string RoleId { get; set; }  // Role identifier within the organization.
 }

 // Represents a role within an organization.
 [Table("OrganizationRoles")]
 public class OrganizationRoleModel
 {
     // Constructor to initialize an OrganizationRole instance.
     public OrganizationRoleModel(string id, string name)
     {
         Id = id;
         Name = name;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the role.
     public string Name { get; set; }  // Name of the role.
 }

 // Represents an organization with its attributes and state.
 [Table("Organizations")]
 public class OrganizationModel
 {
     // Constructor to initialize an Organization instance.
     public OrganizationModel(string id, string name, string createdBy)
     {
         Id = id;
         Name = name;
         CreatedBy = createdBy;
         Created = DateTime.Now;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the organization.
     public string Name { get; set; }  // Name of the organization.
     public DateTime Created { get; set; }  // Date and time the organization was created.
     [MaxLength(36)]
     public string CreatedBy { get; set; }  // User identifier of the creator.
     public bool IsSuspended { get; set; }  // Indicates if the organization is currently suspended.
     public DateTime Suspended { get; set; }  // Date and time the organization was suspended.
     public bool IsDeleted { get; set; }  // Indicates if the organization is marked as deleted.
     public DateTime Deleted { get; set; }  // Date and time the organization was deleted.
 }

カレンダーを定義

次にカレンダー周りを定義していきます。 カレンダーとカレンダーグループ(カレンダーがテーブルや部屋だった場合、カレンダーグループは店舗などを想定)、予約モデルになります。

 // Represents a group of calendars, typically used to organize related calendars.
 [Table("CalendarGroups")]
 public class CalendarGroupModel
 {
     // Constructor to initialize a CalendarGroup with its unique identifier and name.
     public CalendarGroupModel(string id, string name)
     {
         Id = id;
         Name = name;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar group, restricted to 36 characters.
     public string Name { get; set; }  // Name of the calendar group.
 }

 // Represents an item or entry within a calendar group, linking specific calendars to their respective groups.
 [Table("CalendarGroupItems")]
 public class CalendarGroupItemModel
 {
     // Constructor to initialize a CalendarGroupItem with identifiers for the item, its calendar, and its group.
     public CalendarGroupItemModel(string id, string calendarId, string calendarGroupId)
     {
         Id = id;
         CalendarId = calendarId;
         CalendarGroupId = calendarGroupId;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar group item, restricted to 36 characters.
     [MaxLength(36)]
     public string CalendarId { get; set; }  // Identifier of the calendar associated with this item, restricted to 36 characters.
     [MaxLength(36)]
     public string CalendarGroupId { get; set; }  // Identifier of the group to which this item belongs, restricted to 36 characters.
 }

 // Represents a calendar, detailing its name, associated organization, and settings like time zone and visibility.
 [Table("Calendars")]
 public class CalendarModel
 {
     // Constructor to initialize a Calendar with essential attributes.
     public CalendarModel(string id, string name, string organizationId, string timeZone, bool isPublic, string createdBy, DateTime created)
     {
         Id = id;
         Name = name;
         OrganizationId = organizationId;
         TimeZone = timeZone;
         IsPublic = isPublic;
         CreatedBy = createdBy;
         Created = created;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar, restricted to 36 characters.
     public string Name { get; set; }  // Name of the calendar.
     public string? Description { get; set; }  // Optional description of the calendar.
     [MaxLength(36)]
     public string OrganizationId { get; set; }  // Identifier of the organization owning this calendar, restricted to 36 characters.
     [MaxLength(36)]
     public string TimeZone { get; set; }  // Time zone in which the calendar operates, restricted to 36 characters.
     [MaxLength(12)]
     public string? Color { get; set; }  // Optional color code for the calendar, restricted to 12 characters.
     public bool IsPublic { get; set; }  // Indicates if the calendar is public.
     public bool IsDeleted { get; set; }  // Indicates if the calendar has been marked as deleted.
     public string? DefaultLocation { get; set; }  // Optional default location for events in the calendar.
     public int MaxAttendees { get; set; }  // Maximum number of attendees per event.
     public int MinAttendees { get; set; }  // Minimum number of attendees required for an event.
     public int TimeScale { get; set; }  // Time scale granularity for events (in minutes).
     public string CreatedBy { get; set; }
     public DateTime Created { get; set; }

 }

 // Represents a reservation within a calendar, detailing the reservation's timeframe, booker, and status.
 [Table("Reservations")]
 public class ReservationModel
 {
     // Constructor to initialize a Reservation with its essential attributes.
     public ReservationModel(string id, string calendarId, string organizationId, string name, DateTime startFrom, DateTime endAt, bool isWholeDay, string bookerId, string status)
     {
         Id = id;
         CalendarId = calendarId;
         OrganizationId = organizationId;
         Name = name;
         StartFrom = startFrom;
         EndAt = endAt;
         IsWholeDay = isWholeDay;
         BookerId = bookerId;
         Status = status;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the reservation, restricted to 36 characters.
     public string OrganizationId { get; set; }
     public string CalendarId { get; set; }
     public string Name { get; set; }  // Name of the reservation.
     public string? Description { get; set; }  // Optional description of the reservation.
     [MaxLength(36)]
     public string? Color { get; set; }  // Optional color code for the reservation, restricted to 36 characters.
     [MaxLength(36)]
     public string? CartId { get; set; }  // Optional cart identifier if the reservation is part of a booking system, restricted to 36 characters.
     public DateTime StartFrom { get; set; }  // Start time and date of the reservation.
     public DateTime EndAt { get; set; }  // End time and date of the reservation.
     public bool IsWholeDay { get; set; }
     [MaxLength(36)]
     public string BookerId { get; set; }  // Identifier of the person who booked the reservation, restricted to 36 characters.
     [MaxLength(36)]
     public string Status { get; set; }  // Current status of the reservation, restricted to 36 characters.
     public string? UnderName { get; set; }  // Optional name under which the reservation is booked.
     public DateTime Created { get; set; }  // Creation date and time of the reservation.
    
     public bool IsDeleted { get; set; }  // Indicates if the reservation has been marked as deleted.
     public DateTime Deleted { get; set; }  // Date and time when the reservation was marked as deleted.
 }

データの定義ができたら、ApplicationDbContext.cs も更新します。

 public class ApplicationDbContext : IdentityDbContext<UserModel>
 {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
   : base(options)
     {
     }

     public DbSet<OrganizationModel> Organizations { get; set; }
     public DbSet<OrganizationRoleModel> OrganizationRoles { get; set; }
     public DbSet<OrganizationMembershipModel> OrganizationMemberships { get; set; }
     public DbSet<CalendarModel> Calendars { get; set; }
     public DbSet<CalendarGroupModel> CalendarGroups { get; set; }
     public DbSet<CalendarGroupItemModel> CalendarGroupItems { get; set; }
     public DbSet<ReservationModel> Reservations { get; set; }
 }

その後 add-migration update-database でデータベースを更新します。

作成されたテーブルを確認して次に進みます。

image.png

CRUD

データの定義ができたので、次はデータに対するアクションを書いていきます。C(Create) R(Read (Search + Get)) U(Update) D(Delete) の順で書いていきます.

組織のCRUD

まずは、APIに対するInput/Outputのモデルを定義します。

 /// <summary>
 /// Represents a request model for creating an organization.
 /// This model captures all necessary information needed to create a new organization.
 /// </summary>
 public class OrganizationsCreateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsCreateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The name of the organization to be created.</param>
     /// <param name="createdBy">The identifier of the user creating the organization.</param>
     public OrganizationsCreateRequestModel(string name, string createdBy)
     {
         Name = name;
         CreatedBy = createdBy;
     }

     /// <summary>
     /// Gets or sets the name of the organization.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the identifier of the user who is creating the organization.
     /// This can be used for tracking who initiated the creation process.
     /// </summary>
     public string CreatedBy { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after creating an organization.
 /// This model encapsulates the details of the newly created organization.
 /// </summary>
 public class OrganizationsCreateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsCreateResponseModel with the created organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully created.</param>
     public OrganizationsCreateResponseModel(OrganizationModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the created organization.
     /// </summary>
     public OrganizationModel Organization { get; set; }
 }

 /// <summary>
 /// Represents a view model for an organization.
 /// This model is typically used to format or present data in a specific way for views or API responses.
 /// </summary>
 public class OrganizationViewModel
 {
     /// <summary>
     /// Gets or sets the organization details. This property can be null if no organization data is available.
     /// </summary>
     public OrganizationModel? Organization { get; set; }
 }

 /// <summary>
 /// Represents the search criteria for querying organizations.
 /// This model captures the parameters used to filter and page the search results.
 /// </summary>
 public class OrganizationsSearchRequestModel
 {
     /// <summary>
     /// Gets or sets the keyword used for searching organizations by name or other attributes.
     /// This can be null, in which case the filter should not apply.
     /// </summary>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria (e.g., 'Name ASC', 'Name DESC').
     /// This can be null, in which case a default sort should be applied.
     /// </summary>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number in pagination.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page in pagination.
     /// </summary>
     public int ItemsPerPage { get; set; }
 }

 /// <summary>
 /// Represents the response model returned from a search query for organizations.
 /// This model includes the list of organizations that match the search criteria along with additional pagination details.
 /// </summary>
 public class OrganizationsSearchResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsSearchResponseModel with the list of organizations.
     /// </summary>
     /// <param name="organizations">The list of organizations that match the search criteria.</param>
     public OrganizationsSearchResponseModel(List<OrganizationViewModel> organizations)
     {
         Organizations = organizations;
     }

     /// <summary>
     /// Gets or sets the keyword used in the search query.
     /// This can be null if no keyword was used.
     /// </summary>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria used in the search query.
     /// This can be null if no specific sorting was applied.
     /// </summary>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number in the search results pagination.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page in the search results pagination.
     /// </summary>
     public int ItemsPerPage { get; set; }

     /// <summary>
     /// Gets or sets the total number of items that match the search criteria.
     /// </summary>
     public int TotalItems { get; set; }

     /// <summary>
     /// Gets or sets the total number of pages available based on the current pagination settings.
     /// </summary>
     public int TotalPages { get; set; }

     /// <summary>
     /// Gets or sets the list of organization view models that match the search criteria.
     /// </summary>
     public List<OrganizationViewModel> Organizations { get; set; }
 }

 /// <summary>
 /// Represents the response model returned when retrieving an organization by its ID.
 /// This model encapsulates the details of the retrieved organization.
 /// </summary>
 public class OrganizationsGetResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsGetResponseModel with the retrieved organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully retrieved.</param>
     public OrganizationsGetResponseModel(OrganizationViewModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the retrieved organization.
     /// </summary>
     public OrganizationViewModel Organization { get; set; }
 }

 /// <summary>
 /// Represents the request model for updating an organization.
 /// This model captures the necessary information needed to update an existing organization.
 /// </summary>
 public class OrganizationsUpdateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsUpdateRequestModel with the specified name.
     /// </summary>
     /// <param name="name">The new name of the organization.</param>
     public OrganizationsUpdateRequestModel(string name)
     {
         Name = name;
     }

     /// <summary>
     /// Gets or sets the name of the organization to be updated.
     /// </summary>
     public string Name { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after updating an organization.
 /// This model encapsulates the details of the updated organization.
 /// </summary>
 public class OrganizationsUpdateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsUpdateResponseModel with the updated organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully updated.</param>
     public OrganizationsUpdateResponseModel(OrganizationModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the updated organization.
     /// </summary>
     public OrganizationModel Organization { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after deleting an organization.
 /// This model encapsulates the details of the organization that has been deleted.
 /// </summary>
 public class OrganizationsDeleteResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsDeleteResponseModel with the organization that has been deleted.
     /// </summary>
     /// <param name="deletedOrganization">The organization that has been successfully deleted.</param>
     public OrganizationsDeleteResponseModel(OrganizationModel deletedOrganization)
     {
         DeletedOrganization = deletedOrganization;
     }

     /// <summary>
     /// Gets or sets the details of the deleted organization.
     /// </summary>
     public OrganizationModel DeletedOrganization { get; set; }
 }

Create 作成

 /// <summary>
 /// Creates a new organization.
 /// </summary>
 /// <param name="request">The organization creation request containing the name of the organization.</param>
 /// <returns>Returns the newly created organization details.</returns>
 /// <response code="200">Returns the newly created organization</response>
 /// <response code="400">If the request is null or invalid</response>
 /// <response code="500">If there is an internal server error</response>
 [HttpPost("/organization")]
 [SwaggerOperation(
     Summary = "Create a new organization",
     Description = "Creates a new organization with the specified name. Requires user authentication.",
     OperationId = "CreateOrganization",
     Tags = new[] { "Organization" }
 )]
 [SwaggerResponse(statusCode: 200, type: typeof(OrganizationsCreateResponseModel), description: "Returns the newly created organization")]
 [SwaggerResponse(statusCode: 400, description: "If the input is null or invalid")]
 [SwaggerResponse(statusCode: 500, description: "If there is an internal server error")]
 public async Task<ActionResult<OrganizationsCreateResponseModel>> CreateOrganizationAsync([FromBody] OrganizationsCreateRequestModel request)
 {
     if (request == null)
     {
         return BadRequest("Request cannot be null");
     }

     // Retrieve the currently authenticated user
     var user = await _userManager.GetUserAsync(User);
     if (user == null)
     {
         return Unauthorized("User must be logged in to create an organization");
     }

     // Create a new organization object with a unique identifier
     var organization = new OrganizationModel(Guid.NewGuid().ToString(), request.Name, user.Id)
     {
         Created = DateTime.UtcNow // Using UTC to avoid timezone issues
     };

     // Add the new organization to the database
     _db.Organizations.Add(organization);

      // Add creater as a member (admin) of the organization
     _db.OrganizationMemberships.Add(new OrganizationMembershipModel(Guid.NewGuid().ToString(), 
     organization.Id, user.Id, "admin"));
     
     await _db.SaveChangesAsync();

     // Prepare the response model with the created organization details
     var result = new OrganizationsCreateResponseModel(organization);

     return Ok(result);
 }

Read (Search) 検索

/// <summary>
/// Searches organizations based on the specified criteria.
/// </summary>
/// <param name="request">The search request containing keyword, sort criteria, and pagination details.</param>
/// <returns>A list of organizations that match the search criteria along with pagination details.</returns>
/// <response code="200">Returns a list of organizations that match the search criteria</response>
/// <response code="400">If the pagination parameters are invalid</response>
/// <response code="500">If there is an internal server error</response>
[HttpPost("/organization/search")]
[SwaggerOperation(
    Summary = "Search organizations",
    Description = "Searches for organizations based on keywords, sort criteria, and pagination settings. Requires user authentication.",
    OperationId = "SearchOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsSearchResponseModel), description: "Successful search results with pagination")]
[SwaggerResponse(statusCode: 400, description: "Invalid pagination parameters")]
[SwaggerResponse(statusCode: 500, description: "Internal server error")]
public ActionResult<OrganizationsSearchResponseModel> SearchOrganization([FromBody] OrganizationsSearchRequestModel request)
{
    // Input validation
    if (request.CurrentPage <= 0)
    {
        return BadRequest("CurrentPage must be greater than 0.");
    }
    if (request.ItemsPerPage <= 0)
    {
        return BadRequest("ItemsPerPage must be greater than 0.");
    }

    // Query organizations based on keyword search
    var organizations = from o in _db.Organizations select o;
    if (!string.IsNullOrEmpty(request.Keyword))
    {
        organizations = organizations.Where(o => o.Name.Contains(request.Keyword));
    }

    // Count total items to support pagination
    int totalItems = organizations.Count();
    var items = organizations
                .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                .Take(request.ItemsPerPage)
                .Select(r => new OrganizationViewModel { Organization = r })
                .ToList();

    // Prepare the response model with pagination info
    var result = new OrganizationsSearchResponseModel(items)
    {
        Keyword = request.Keyword,
        Sort = request.Sort,
        CurrentPage = request.CurrentPage,
        ItemsPerPage = request.ItemsPerPage,
        TotalItems = totalItems,
    };

    return Ok(result);
}
        

Read (Get) 取得

 /// <summary>
 /// Retrieves an organization by its ID.
 /// </summary>
 /// <param name="id">The unique identifier of the organization to retrieve.</param>
 /// <returns>Returns the organization details if found; otherwise, returns a NotFound result.</returns>
 /// <response code="200">Returns the organization details if found</response>
 /// <response code="404">If no organization is found with the provided ID</response>
 [HttpGet("/organization/{id}")]
 [SwaggerOperation(
     Summary = "Retrieve an organization by ID",
     Description = "Retrieves the details of an organization based on the unique identifier provided. Requires user authentication.",
     OperationId = "GetOrganization",
     Tags = new[] { "Organization" }
 )]
 [SwaggerResponse(statusCode: 200, type: typeof(OrganizationsGetResponseModel), description: "Successful retrieval of the organization")]
 [SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
 public ActionResult<OrganizationsGetResponseModel> GetOrganization([FromRoute] string id)
 {
     // Query the database for the organization with the specified ID
     var organization = (from o in _db.Organizations
                         where o.Id == id
                         select new OrganizationViewModel
                         {
                             Organization = o
                         }).FirstOrDefault();

     // Check if the organization was found
     if (organization == null)
     {
         return NotFound();
     }

     // Prepare the response model with the found organization details
     var result = new OrganizationsGetResponseModel(organization);

     return Ok(result);
 }

Update 更新

/// <summary>
/// Updates an existing organization.
/// </summary>
/// <param name="id">The unique identifier of the organization to update.</param>
/// <param name="request">The request model containing updated fields for the organization.</param>
/// <returns>Returns the updated organization details. If the organization is not found, returns NotFound.</returns>
/// <response code="200">Returns the updated organization details</response>
/// <response code="404">If no organization is found with the provided ID</response>
/// <response code="400">If the request data is invalid</response>
[HttpPut("/organization/{id}")]
[SwaggerOperation(
    Summary = "Update an existing organization",
    Description = "Updates the specified fields of an existing organization. Requires user authentication and the organization ID in the route.",
    OperationId = "UpdateOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsUpdateResponseModel), description: "Successful update of the organization")]
[SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
[SwaggerResponse(statusCode: 400, description: "Invalid data in request")]
public async Task<ActionResult<OrganizationsUpdateResponseModel>> UpdateOrganization(
    [FromRoute] string id,
    [FromBody] OrganizationsUpdateRequestModel request)
{
    // Attempt to find the existing organization by ID
    var original = await _db.Organizations.FindAsync(id);

    // Check if the organization exists
    if (original == null)
    {
        return NotFound();
    }

    // Update the organization's properties
    original.Name = request.Name;
    // Add more fields to update as necessary

    // Save the updated organization back to the database
    _db.Organizations.Update(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the updated organization details
    var result = new OrganizationsUpdateResponseModel(original);

    return Ok(result);
}

Delete 削除

ここでは物理削除していますが、論理削除でもいいかと思います。

/// <summary>
/// Deletes an organization by its ID.
/// </summary>
/// <param name="id">The unique identifier of the organization to be deleted.</param>
/// <returns>Returns a response indicating the result of the deletion process.</returns>
/// <response code="200">Returns the details of the deleted organization</response>
/// <response code="404">If no organization is found with the provided ID</response>
[HttpDelete("/organization/{id}")]
[SwaggerOperation(
    Summary = "Delete an organization",
    Description = "Deletes an existing organization based on the unique identifier provided. Requires user authentication.",
    OperationId = "DeleteOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsDeleteResponseModel), description: "Successful deletion of the organization")]
[SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
public async Task<ActionResult<OrganizationsDeleteResponseModel>> DeleteOrganizationAsync(
    [FromRoute] string id)
{
    // Attempt to find the organization by ID
    var original = await _db.Organizations.FindAsync(id);

    // Check if the organization exists
    if (original == null)
    {
        return NotFound();
    }

    // Remove the organization from the database
    _db.Organizations.Remove(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the deleted organization details
    var result = new OrganizationsDeleteResponseModel(original);

    return Ok(result);
}

Swaggerde確認、一旦よさそうなので進めます。

image.png

カレンダーのCRUD

次は組織内にあるカレンダーのCRUDを作ります。

同じくまずは、APIに対するInput/Outputのモデルを定義します。

 /// <summary>
 /// Represents the request model for creating a new calendar.
 /// This model includes all necessary details required to create a calendar within an organization.
 /// </summary>
 public class CalendarCreateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarCreateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The name of the calendar.</param>
     /// <param name="timeZone">The time zone in which the calendar operates.</param>
     /// <param name="isPublic">Indicates whether the calendar is public or private.</param>
     /// <param name="maxAttendees">The maximum number of attendees allowed per event.</param>
     /// <param name="minAttendees">The minimum number of attendees required for an event.</param>
     /// <param name="timeScale">The time scale in minutes used for calendar events.</param>
     /// <param name="createdBy">The identifier of the user creating the calendar.</param>
     /// <param name="created">The date and time when the calendar was created.</param>
     public CalendarCreateRequestModel(string name, string timeZone,
         bool isPublic, int maxAttendees, int minAttendees, int timeScale, string createdBy,
         DateTime created)
     {
         Name = name;
         TimeZone = timeZone;
         IsPublic = isPublic;
         MaxAttendees = maxAttendees;
         MinAttendees = minAttendees;
         TimeScale = timeScale;
         CreatedBy = createdBy;
         Created = created;
     }

     /// <summary>
     /// Gets or sets the name of the calendar.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the description of the calendar. Optional.
     /// </summary>
     public string? Description { get; set; }

     /// <summary>
     /// Gets or sets the time zone of the calendar.
     /// </summary>
     public string TimeZone { get; set; }

     /// <summary>
     /// Gets or sets the color associated with the calendar. Optional.
     /// </summary>
     public string? Color { get; set; }

     /// <summary>
     /// Gets or sets a value indicating whether the calendar is public.
     /// </summary>
     public bool IsPublic { get; set; }

     /// <summary>
     /// Gets or sets the default location for events in the calendar. Optional.
     /// </summary>
     public string? DefaultLocation { get; set; }

     /// <summary>
     /// Gets or sets the maximum number of attendees allowed per event.
     /// </summary>
     public int MaxAttendees { get; set; }

     /// <summary>
     /// Gets or sets the minimum number of attendees required for an event.
     /// </summary>
     public int MinAttendees { get; set; }

     /// <summary>
     /// Gets or sets the time scale, in minutes, for events in the calendar.
     /// </summary>
     public int TimeScale { get; set; }

     /// <summary>
     /// Gets or sets the identifier of the user who created the calendar.
     /// </summary>
     public string CreatedBy { get; set; }

     /// <summary>
     /// Gets or sets the date and time when the calendar was created.
     /// </summary>
     public DateTime Created { get; set; }
 }


 /// <summary>
 /// Represents the response model for a calendar creation request.
 /// This model contains the details of the newly created calendar, encapsulated within a CalendarViewModel.
 /// </summary>
 public class CalendarCreateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarCreateResponseModel with the specified calendar details.
     /// </summary>
     /// <param name="calendar">The calendar view model that includes the details of the newly created calendar.</param>
     public CalendarCreateResponseModel(CalendarViewModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarViewModel that includes the details of the newly created calendar.
     /// </summary>
     public CalendarViewModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the search criteria for querying calendars.
 /// This model includes pagination parameters and can optionally include search keywords and sorting instructions.
 /// </summary>
 public class CalendarsSearchRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarsSearchRequestModel with specified pagination details.
     /// </summary>
     /// <param name="currentPage">The page number of the search results to retrieve.</param>
     /// <param name="itemsPerPage">The number of items to display per page in the search results.</param>
     public CalendarsSearchRequestModel(int currentPage, int itemsPerPage)
     {
         CurrentPage = currentPage;
         ItemsPerPage = itemsPerPage;
     }

     /// <summary>
     /// Gets or sets the search keyword to filter the calendars. Optional.
     /// </summary>
     /// <remarks>
     /// If provided, the search will include only calendars that contain this keyword in their searchable fields.
     /// </remarks>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria for the search results. Optional.
     /// </summary>
     /// <remarks>
     /// Example formats include "Name asc" or "Created desc". If not provided, a default sort may be applied.
     /// </remarks>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number of the search results.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page to be returned in the search results.
     /// </summary>
     public int ItemsPerPage { get; set; }
 }
 /// <summary>
 /// Represents the response model for a search query on calendars.
 /// This model provides a structured format for pagination and includes the results of the search.
 /// </summary>
 public class CalendarsSearchResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarsSearchResponseModel with a list of calendar view models.
     /// </summary>
     /// <param name="calendars">A list of CalendarViewModels that represent the search results.</param>
     public CalendarsSearchResponseModel(List<CalendarViewModel> calendars)
     {
         Items = calendars;
     }

     /// <summary>
     /// Gets or sets the keyword used in the search query. Optional.
     /// </summary>
     /// <remarks>
     /// If provided, this was used to filter the results based on searchable fields within the calendar data.
     /// </remarks>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria used in the search query. Optional.
     /// </summary>
     /// <remarks>
     /// Examples include "Name asc" or "Created desc". If not provided, a default sort may have been applied.
     /// </remarks>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number of the search results, indicating where in the pagination the returned data is situated.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page that were returned in this segment of the search results.
     /// </summary>
     public int ItemsPerPage { get; set; }

     /// <summary>
     /// Gets or sets the total number of items that matched the search criteria, useful for calculating the total number of pages.
     /// </summary>
     public int TotalItems { get; set; }

     /// <summary>
     /// Gets or sets the total number of pages available based on the current pagination settings.
     /// </summary>
     public int TotalPages { get; set; }

     /// <summary>
     /// Gets or sets the list of CalendarViewModels that represent the search results.
     /// </summary>
     public List<CalendarViewModel> Items { get; set; }
 }

 /// <summary>
 /// Represents the response model for retrieving a calendar.
 /// This model is used to provide detailed information about a specific calendar following a retrieval request.
 /// </summary>
 public class CalendarGetResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarGetResponseModel with the specified calendar view model.
     /// </summary>
     /// <param name="calendar">The calendar view model that includes all relevant details of the retrieved calendar.</param>
     public CalendarGetResponseModel(CalendarViewModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarViewModel that includes all the details of the retrieved calendar.
     /// </summary>
     public CalendarViewModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the view model for a calendar.
 /// This model is used primarily for data transfer within the API, especially for encapsulating calendar details in responses.
 /// </summary>
 public class CalendarViewModel
 {
     /// <summary>
     /// Gets or sets the CalendarModel that contains detailed information about the calendar.
     /// This property may be null if the specific calendar details are not available or not necessary for the context in which the view model is used.
     /// </summary>
     public CalendarModel? Calendar { get; set; }
 }

 /// <summary>
 /// Represents the request model for updating an existing calendar.
 /// This model captures all necessary details required to modify a calendar within an organization.
 /// </summary>
 public class CalendarUpdateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarUpdateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The new name of the calendar.</param>
     /// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
     /// <param name="timeZone">The time zone in which the calendar operates.</param>
     /// <param name="isPublic">Indicates whether the calendar is public or private.</param>
     /// <param name="maxAttendees">The maximum number of attendees allowed per event.</param>
     /// <param name="minAttendees">The minimum number of attendees required for an event.</param>
     /// <param name="timeScale">The time scale in minutes used for calendar events.</param>
     public CalendarUpdateRequestModel(string name, string organizationId, string timeZone,
         bool isPublic, int maxAttendees, int minAttendees, int timeScale)
     {
         Name = name;
         OrganizationId = organizationId;
         TimeZone = timeZone;
         IsPublic = isPublic;
         MaxAttendees = maxAttendees;
         MinAttendees = minAttendees;
         TimeScale = timeScale;
     }

     /// <summary>
     /// Gets or sets the name of the calendar.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the optional description of the calendar.
     /// </summary>
     public string? Description { get; set; }

     /// <summary>
     /// Gets or sets the ID of the organization to which the calendar belongs.
     /// </summary>
     public string OrganizationId { get; set; }

     /// <summary>
     /// Gets or sets the time zone of the calendar.
     /// </summary>
     public string TimeZone { get; set; }

     /// <summary>
     /// Gets or sets the optional color associated with the calendar.
     /// </summary>
     public string? Color { get; set; }

     /// <summary>
     /// Gets or sets a value indicating whether the calendar is public.
     /// </summary>
     public bool IsPublic { get; set; }

     /// <summary>
     /// Gets or sets the optional default location for events in the calendar.
     /// </summary>
     public string? DefaultLocation { get; set; }

     /// <summary>
     /// Gets or sets the maximum number of attendees allowed per event.
     /// </summary>
     public int MaxAttendees { get; set; }

     /// <summary>
     /// Gets or sets the minimum number of attendees required for an event.
     /// </summary>
     public int MinAttendees { get; set; }

     /// <summary>
     /// Gets or sets the time scale, in minutes, for events in the calendar.
     /// </summary>
     public int TimeScale { get; set; }
 }

 /// <summary>
 /// Represents the response model for an update request on a calendar.
 /// This model provides the details of the updated calendar, ensuring that the requester can verify the new state of the calendar post-update.
 /// </summary>
 public class CalendarUpdateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarUpdateResponseModel with the updated calendar details.
     /// </summary>
     /// <param name="calendar">The calendar model that includes all updated details of the calendar.</param>
     public CalendarUpdateResponseModel(CalendarModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarModel that includes the details of the updated calendar.
     /// </summary>
     public CalendarModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the response model for a calendar deletion request.
 /// This model provides details of the calendar that has been deleted, allowing for verification and record-keeping.
 /// </summary>
 public class CalendarDeleteResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarDeleteResponseModel with the specified deleted calendar details.
     /// </summary>
     /// <param name="deletedCalendar">The calendar model that includes all relevant details of the deleted calendar.</param>
     public CalendarDeleteResponseModel(CalendarModel deletedCalendar)
     {
         Calendar = deletedCalendar;
     }

     /// <summary>
     /// Gets or sets the CalendarModel that contains the details of the deleted calendar.
     /// This property provides a final snapshot of the calendar prior to its deletion.
     /// </summary>
     public CalendarModel Calendar { get; set; }
 }

Create 新規作成

 /// <summary>
 /// Creates a new calendar associated with a specified organization.
 /// </summary>
 /// <param name="organizationId">The ID of the organization to which the calendar will be associated.</param>
 /// <param name="request">The details of the calendar to be created.</param>
 /// <returns>Returns the created calendar details or appropriate error messages.</returns>
 /// <remarks>
 /// Sample request:
 ///
 ///     POST /{organizationId}/calendar
 ///     {
 ///        "name": "Team Meetings",
 ///        "timeZone": "Eastern Standard Time",
 ///        "isPublic": true,
 ///        "description": "Calendar for all team meetings",
 ///        "defaultLocation": "Board Room",
 ///        "color": "blue",
 ///        "maxAttendees": 50,
 ///        "minAttendees": 1,
 ///        "timeScale": 15
 ///     }
 ///
 /// </remarks>
 /// <response code="200">Returns the newly created calendar</response>
 /// <response code="400">If the request is null or parameters are missing</response>
 /// <response code="401">If the user is not authenticated</response>
 /// <response code="404">If the specified organization is not found</response>
 [HttpPost("/{organizationId}/calendar")]
 [SwaggerOperation(Summary = "Create a new calendar", Description = "Creates a new calendar associated with a specified organization.")]
 [SwaggerResponse(statusCode: 200, type: typeof(CalendarCreateResponseModel), description: "Successfully created the calendar")]
 [SwaggerResponse(statusCode: 400, description: "Bad request if the request body is null or missing required fields")]
 [SwaggerResponse(statusCode: 401, description: "Unauthorized if the user is not logged in")]
 [SwaggerResponse(statusCode: 404, description: "Not found if no organization matches the provided ID")]
 public async Task<ActionResult<CalendarCreateResponseModel>> CreateCalendarAsync(
     [FromQuery] string organizationId,
     [FromBody] CalendarCreateRequestModel request)
 {
     if (request == null)
     {
         return BadRequest("Request cannot be null");
     }

     // Retrieve the currently authenticated user
     var user = await _userManager.GetUserAsync(User);
     if (user == null)
     {
         return Unauthorized("User must be logged in to create an organization");
     }

     // Make sure the organization exists
     var organization = _db.Organizations.Find(organizationId);
     if (organization == null)
     {
         return NotFound("Organization not found");
     }

     var calendar = new CalendarModel(Guid.NewGuid().ToString(), request.Name,
         organization.Id, request.TimeZone, request.IsPublic, user.Id, DateTime.UtcNow)
     {
         Description = request.Description,
         DefaultLocation = request.DefaultLocation,
         Color = request.Color,
         MaxAttendees = request.MaxAttendees,
         MinAttendees = request.MinAttendees,
         TimeScale = request.TimeScale
     };

     _db.Calendars.Add(calendar);
     await _db.SaveChangesAsync();

     var result = new CalendarCreateResponseModel(new CalendarViewModel()
     {
         Calendar = calendar
     });

     return Ok(result);
 }

Read (Search) 検索

/// <summary>
/// Searches calendars within an organization based on the provided criteria.
/// </summary>
/// <param name="organizationId">The ID of the organization whose calendars are being searched.</param>
/// <param name="request">The search criteria including keyword, pagination, and sorting information.</param>
/// <returns>Returns a list of calendars that match the search criteria along with pagination details.</returns>
/// <remarks>
/// Sample request:
///
///     POST /{organizationId}/calendar/search
///     {
///        "keyword": "Team",
///        "sort": "name",
///        "currentPage": 1,
///        "itemsPerPage": 10
///     }
///
/// </remarks>
/// <response code="200">Returns the list of calendars that match the search criteria</response>
/// <response code="400">If the pagination parameters are invalid</response>
[HttpPost("/{organizationId}/calendar/search")]
[SwaggerOperation(Summary = "Search calendars", Description = "Searches for calendars within an organization based on the provided criteria.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarsSearchResponseModel), description: "Successful retrieval of the list of calendars")]
[SwaggerResponse(statusCode: 400, description: "Bad request if the pagination parameters are incorrect")]
public ActionResult<CalendarsSearchResponseModel> SearchCalendar(
    [FromRoute] string organizationId, [FromBody] CalendarsSearchRequestModel request)
{
    // Input validation
    if (request.CurrentPage <= 0)
    {
        return BadRequest("CurrentPage must be greater than 0.");
    }
    if (request.ItemsPerPage <= 0)
    {
        return BadRequest("ItemsPerPage must be greater than 0.");
    }

    var calendars = from o in _db.Calendars where o.OrganizationId == organizationId select o;
    if (!string.IsNullOrEmpty(request.Keyword))
    {
        calendars = calendars.Where(o => o.Name.Contains(request.Keyword));
    }

    // Count total items to support pagination
    int totalItems = calendars.Count();
    var items = calendars
                .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                .Take(request.ItemsPerPage)
                .Select(r => new CalendarViewModel { Calendar = r })
                .ToList();

    // Prepare the response model with pagination info
    var result = new CalendarsSearchResponseModel(items)
    {
        Keyword = request.Keyword,
        Sort = request.Sort,
        CurrentPage = request.CurrentPage,
        ItemsPerPage = request.ItemsPerPage,
        TotalItems = totalItems,
        TotalPages = (int)Math.Ceiling((double)totalItems / request.ItemsPerPage)
    };

    return Ok(result);
}

Read (Get) 取得

ここではorganizationIdを確認していますが、本来現在ログインしているユーザーが組織にいて、ロールを持っているか確認するべきだと思いますが、その辺りは追々追加していこうと思います。

/// <summary>
/// Retrieves a calendar by its ID within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
/// <param name="id">The ID of the calendar to retrieve.</param>
/// <returns>Returns the calendar details if found or a not found error if no calendar is found with the provided ID within the organization.</returns>
/// <response code="200">Returns the calendar details if found</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
[HttpGet("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Retrieve a calendar", Description = "Retrieves a calendar by its ID within a specified organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarGetResponseModel), description: "Successfully retrieved the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
public ActionResult<CalendarGetResponseModel> GetCalendar(
    [FromRoute] string organizationId,
    [FromRoute] string id)
{
    // Query the database for the calendar with the specified ID and ensure it belongs to the specified organization
    var calendar = (from o in _db.Calendars
                    where o.Id == id && o.OrganizationId == organizationId
                    select new CalendarViewModel
                    {
                        Calendar = o
                    }).FirstOrDefault();

    // Check if the calendar was found
    if (calendar == null)
    {
        return NotFound();
    }

    var result = new CalendarGetResponseModel(calendar);

    return Ok(result);
}

Update 更新

/// <summary>
/// Updates a calendar within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
/// <param name="id">The ID of the calendar to update.</param>
/// <param name="request">The updated data for the calendar.</param>
/// <returns>Returns the updated calendar details or appropriate error messages.</returns>
/// <response code="200">If the calendar is successfully updated</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
/// <response code="401">If the calendar does not belong to the given organization or the user is not authorized</response>
[HttpPut("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Update a calendar", Description = "Updates a specific calendar within a specified organization based on the provided calendar ID.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarUpdateResponseModel), description: "Successfully updated the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
[SwaggerResponse(statusCode: 401, description: "Unauthorized if the calendar does not belong to the given organization")]
public async Task<ActionResult<CalendarUpdateResponseModel>> UpdateCalendar(
    [FromRoute] string organizationId, [FromRoute] string id, [FromBody] CalendarUpdateRequestModel request)
{
    // Attempt to find the existing calendar by ID
    var original = await _db.Calendars.FindAsync(id);

    // Check if the calendar exists and belongs to the correct organization
    if (original == null)
    {
        return NotFound("The calendar with the specified ID was not found.");
    }

    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The calendar does not belong to the specified organization.");
    }

    // Update the calendar's properties
    original.Name = request.Name;
    original.Description = request.Description; // Example of updating more fields
    original.Color = request.Color;
    original.DefaultLocation = request.DefaultLocation;
    original.IsPublic = request.IsPublic;
    original.MaxAttendees = request.MaxAttendees;
    original.MinAttendees = request.MinAttendees;
    original.TimeScale = request.TimeScale;
    original.TimeZone = request.TimeZone;

    // Save the updated calendar back to the database
    _db.Calendars.Update(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the updated calendar details
    var result = new CalendarUpdateResponseModel(original);

    return Ok(result);
}
        

Delete 削除

/// <summary>
/// Deletes a calendar within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization from which the calendar is to be deleted.</param>
/// <param name="id">The ID of the calendar to delete.</param>
/// <returns>Returns a confirmation of the deletion or appropriate error messages.</returns>
/// <response code="200">If the calendar is successfully deleted</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
/// <response code="401">If the calendar does not belong to the given organization or the user is not authorized</response>
[HttpDelete("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Delete a calendar", Description = "Deletes a specific calendar within a specified organization based on the provided calendar ID.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarDeleteResponseModel), description: "Successfully deleted the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
[SwaggerResponse(statusCode: 401, description: "Unauthorized if the calendar does not belong to the given organization")]
public async Task<ActionResult<CalendarDeleteResponseModel>> DeleteCalendarAsync(
    [FromRoute] string organizationId,
    [FromRoute] string id)
{
    // Attempt to find the calendar by ID
    var original = await _db.Calendars.FindAsync(id);

    // Check if the calendar exists
    if (original == null)
    {
        return NotFound("The calendar with the specified ID was not found.");
    }

    // Check if the calendar belongs to the specified organization
    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The calendar does not belong to the specified organization.");
    }

    // Remove the calendar from the database
    _db.Calendars.Remove(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the deleted calendar details
    var result = new CalendarDeleteResponseModel(original);

    return Ok(result);
}

ここで再度Swaggerde確認、一旦よさそうなので進めます。

image.png

予約のCRUD

次はカレンダー内にある予約のCRUDを作ります。

同じくまずは、APIに対するInput/Outputのモデルを定義します。


    /// <summary>
    /// Represents the request model for creating a new reservation.
    /// This model includes all necessary details required to create a reservation within a system.
    /// </summary>
    public class ReservationCreateRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationCreateRequestModel with specified reservation details.
        /// </summary>
        /// <param name="name">The name of the reservation.</param>
        /// <param name="startFrom">The start time and date of the reservation.</param>
        /// <param name="endAt">The end time and date of the reservation.</param>
        /// <param name="isWholeDay">Indicates whether the reservation spans the whole day.</param>
        /// <param name="bookerId">The identifier of the person who made the reservation.</param>
        /// <param name="status">The current status of the reservation.</param>
        /// <param name="created">The date and time when the reservation was created.</param>
        public ReservationCreateRequestModel(string name, DateTime startFrom, DateTime endAt,
            bool isWholeDay, string bookerId, string status, DateTime created)
        {
            Name = name;
            StartFrom = startFrom;
            EndAt = endAt;
            IsWholeDay = isWholeDay;
            BookerId = bookerId;
            Status = status;
            Created = created;
        }

        /// <summary>
        /// Name of the reservation.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Optional description of the reservation.
        /// </summary>
        public string? Description { get; set; }

        /// <summary>
        /// Optional color code for the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? Color { get; set; }

        /// <summary>
        /// Optional cart identifier if the reservation is part of a booking system, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? CartId { get; set; }

        /// <summary>
        /// Start time and date of the reservation.
        /// </summary>
        public DateTime StartFrom { get; set; }

        /// <summary>
        /// End time and date of the reservation.
        /// </summary>
        public DateTime EndAt { get; set; }

        /// <summary>
        /// Indicates whether the reservation spans the entire day.
        /// </summary>
        public bool IsWholeDay { get; set; }

        /// <summary>
        /// Identifier of the person who booked the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string BookerId { get; set; }

        /// <summary>
        /// Current status of the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string Status { get; set; }

        /// <summary>
        /// Optional name under which the reservation is booked.
        /// </summary>
        public string? UnderName { get; set; }

        /// <summary>
        /// Creation date and time of the reservation.
        /// </summary>
        public DateTime Created { get; set; }
    }
    
    /// <summary>
    /// Represents the response model for a successfully created reservation.
    /// This model contains the details of the newly created reservation, encapsulated within a ReservationViewModel.
    /// </summary>
    public class ReservationCreateResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationCreateResponseModel with the specified reservation details.
        /// </summary>
        /// <param name="reservation">The reservation view model that includes all relevant details of the newly created reservation.</param>
        public ReservationCreateResponseModel(ReservationViewModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationViewModel that includes the details of the newly created reservation.
        /// </summary>
        public ReservationViewModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents the request model for searching reservations based on pagination and optional filtering criteria.
    /// This model allows users to query reservations by page and apply filters such as keywords or sorting.
    /// </summary>
    public class ReservationsSearchRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationsSearchRequestModel with specified pagination details.
        /// </summary>
        /// <param name="currentPage">The page number of the search results to retrieve.</param>
        /// <param name="itemsPerPage">The number of items to display per page in the search results.</param>
        public ReservationsSearchRequestModel(int currentPage, int itemsPerPage)
        {
            CurrentPage = currentPage;
            ItemsPerPage = itemsPerPage;
        }

        /// <summary>
        /// Gets or sets the search keyword to filter the reservations. Optional.
        /// </summary>
        /// <remarks>
        /// If provided, the search will include only reservations that contain this keyword in their searchable fields.
        /// This is useful for quickly locating reservations by relevant identifiers or descriptions.
        /// </remarks>
        public string? Keyword { get; set; }

        /// <summary>
        /// Gets or sets the sorting criteria for the search results. Optional.
        /// </summary>
        /// <remarks>
        /// Sort criteria should be specified in the format "FieldName direction", such as "Name asc" or "Created desc".
        /// If not provided, a default sorting may be applied based on the internal logic of the application.
        /// </remarks>
        public string? Sort { get; set; }

        /// <summary>
        /// Gets or sets the current page number of the search results, facilitating pagination.
        /// </summary>
        public int CurrentPage { get; set; }

        /// <summary>
        /// Gets or sets the number of items per page to be returned in the search results.
        /// This helps in managing the volume of data returned and supports efficient navigation through large sets of data.
        /// </summary>
        public int ItemsPerPage { get; set; }
    }

    /// <summary>
    /// Represents the response model for a search operation on reservations.
    /// This model encapsulates the results and pagination details, providing a structured response to search queries.
    /// </summary>
    public class ReservationsSearchResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationsSearchResponseModel with a list of ReservationViewModels.
        /// These models represent the search results, formatted according to the specified search and pagination parameters.
        /// </summary>
        /// <param name="reservations">A list of ReservationViewModels that represent the search results.</param>
        public ReservationsSearchResponseModel(List<ReservationViewModel> reservations)
        {
            Items = reservations;
        }

        /// <summary>
        /// Gets or sets the keyword used in the search query, if any. This is used to filter the results based on searchable fields within the reservation data.
        /// </summary>
        /// <remarks>
        /// Providing a keyword helps to refine search results to only include items that contain the keyword in relevant fields.
        /// </remarks>
        public string? Keyword { get; set; }

        /// <summary>
        /// Gets or sets the sorting criteria used in the search query. This parameter is optional and determines the order of the search results.
        /// </summary>
        /// <remarks>
        /// Examples include "Name asc" or "Created desc". If no sorting parameter is provided, a default sort order may be applied.
        /// </remarks>
        public string? Sort { get; set; }

        /// <summary>
        /// Gets or sets the current page number of the search results. This parameter helps in navigating through paginated data.
        /// </summary>
        public int CurrentPage { get; set; }

        /// <summary>
        /// Gets or sets the number of items per page that were returned in this segment of the search results.
        /// This controls how much data is presented to the user at one time and aids in pagination control.
        /// </summary>
        public int ItemsPerPage { get; set; }

        /// <summary>
        /// Gets or sets the total number of items that matched the search criteria. This is crucial for calculating the total number of pages.
        /// </summary>
        public int TotalItems { get; set; }

        /// <summary>
        /// Gets or sets the total number of pages available, calculated based on the total number of items and the number of items per page.
        /// </summary>
        public int TotalPages { get; set; }

        /// <summary>
        /// Gets or sets the list of ReservationViewModels that represent the search results. Each model contains details of a reservation found in the search.
        /// </summary>
        public List<ReservationViewModel> Items { get; set; }
    }

    /// <summary>
    /// Represents the response model for retrieving a reservation.
    /// This model is used to provide detailed information about a specific reservation following a retrieval request.
    /// </summary>
    public class ReservationGetResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationGetResponseModel with the specified reservation view model.
        /// This constructor sets up the response model with all relevant details of the retrieved reservation.
        /// </summary>
        /// <param name="reservation">The reservation view model that includes all relevant details of the retrieved reservation.</param>
        public ReservationGetResponseModel(ReservationViewModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationViewModel that includes all the details of the retrieved reservation.
        /// This property provides access to detailed attributes and values of the reservation, such as date, time, participants, and status, among others.
        /// </summary>
        public ReservationViewModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents a view model for a reservation. This model is typically used to encapsulate the reservation data 
    /// that is transferred between the backend and the frontend layers, making it easier to manage data transformations 
    /// and customizations specific to the view requirements.
    /// </summary>
    public class ReservationViewModel
    {
        /// <summary>
        /// Gets or sets the ReservationModel. This property may contain the detailed information of a reservation, 
        /// including dates, times, participant details, and other relevant reservation metadata.
        /// </summary>
        /// <remarks>
        /// The property is nullable, meaning that there may be contexts where a reservation detail is not required 
        /// or not available. This flexibility allows the view model to be used in a variety of scenarios, such as 
        /// creating new reservations or updating existing ones without fully specifying all details initially.
        /// </remarks>
        public ReservationModel? Reservation { get; set; }
    }

    /// <summary>
    /// Represents the request model for updating an existing reservation.
    /// This model captures all necessary details required to modify a reservation within an organization,
    /// including scheduling details, participant information, and reservation status.
    /// </summary>
    public class ReservationUpdateRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationUpdateRequestModel with mandatory parameters for updating a reservation.
        /// </summary>
        /// <param name="name">Name of the reservation.</param>
        /// <param name="startFrom">Start time and date of the reservation.</param>
        /// <param name="endAt">End time and date of the reservation.</param>
        /// <param name="isWholeDay">Indicates whether the reservation spans the entire day.</param>
        /// <param name="bookerId">Identifier of the person who booked the reservation.</param>
        /// <param name="status">Current status of the reservation.</param>
        public ReservationUpdateRequestModel(string name, DateTime startFrom, DateTime endAt,
            bool isWholeDay, string bookerId, string status)
        {
            Name = name;
            StartFrom = startFrom;
            EndAt = endAt;
            IsWholeDay = isWholeDay;
            BookerId = bookerId;
            Status = status;
        }

        /// <summary>
        /// Name of the reservation.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Optional description providing additional details about the reservation.
        /// </summary>
        public string? Description { get; set; }

        /// <summary>
        /// Optional color code for the reservation, used for visual identification, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? Color { get; set; }

        /// <summary>
        /// Optional cart identifier, used if the reservation is part of a booking system, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? CartId { get; set; }

        /// <summary>
        /// Start time and date of the reservation, defining when the reservation begins.
        /// </summary>
        public DateTime StartFrom { get; set; }

        /// <summary>
        /// End time and date of the reservation, defining when the reservation concludes.
        /// </summary>
        public DateTime EndAt { get; set; }

        /// <summary>
        /// Indicates whether the reservation is booked for the entire day.
        /// </summary>
        public bool IsWholeDay { get; set; }

        /// <summary>
        /// Identifier of the person who made the reservation, providing a link to the responsible party, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string BookerId { get; set; }

        /// <summary>
        /// Current status of the reservation, such as 'Confirmed', 'Cancelled', etc., restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string Status { get; set; }

        /// <summary>
        /// Optional name under which the reservation is booked, providing an alternative reference or alias for the booking.
        /// </summary>
        public string? UnderName { get; set; }

        /// <summary>
        /// Indicates if the reservation has been marked as deleted, providing a flag for soft deletion scenarios.
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// Date and time when the reservation was marked as deleted, used in tracking changes and managing records.
        /// </summary>
        public DateTime Deleted { get; set; }
    }
    /// <summary>
    /// Represents the response model for an update request on a reservation.
    /// This model provides the details of the updated reservation, ensuring that the requester can verify the new state of the reservation post-update.
    /// </summary>
    public class ReservationUpdateResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationUpdateResponseModel with the updated reservation details.
        /// </summary>
        /// <param name="reservation">The reservation model that includes all updated details of the reservation.</param>
        public ReservationUpdateResponseModel(ReservationModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationModel that includes the details of the updated reservation.
        /// This property encapsulates the entire set of reservation details after they have been modified,
        /// allowing for a comprehensive view of the updated reservation state.
        /// </summary>
        public ReservationModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents the response model for a reservation deletion request.
    /// This model provides details of the reservation that has been deleted, allowing for verification and record-keeping.
    /// </summary>
    public class ReservationDeleteResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationDeleteResponseModel with the specified deleted reservation details.
        /// This constructor sets up the model with a snapshot of the reservation as it was just before deletion, 
        /// aiding in confirming the correct reservation was removed and providing a record for audit purposes.
        /// </summary>
        /// <param name="deletedReservation">The reservation model that includes all relevant details of the deleted reservation.</param>
        public ReservationDeleteResponseModel(ReservationModel deletedReservation)
        {
            Reservation = deletedReservation;
        }

        /// <summary>
        /// Gets or sets the ReservationModel that contains the details of the deleted reservation.
        /// This property provides a final snapshot of the reservation prior to its deletion, useful for logging, auditing, or
        /// other post-deletion processes that might require a record of the reservation's final state.
        /// </summary>
        public ReservationModel Reservation { get; set; }
    }

Create 作成

/// <summary>
/// Creates a new reservation within a specified calendar and organization.
/// </summary>
/// <param name="organizationId">The ID of the organization under which the reservation is made.</param>
/// <param name="calendarId">The ID of the calendar under which the reservation is made.</param>
/// <param name="request">The reservation details.</param>
/// <returns>Returns the created reservation details or appropriate error messages.</returns>
/// <remarks>
/// Sample request:
///
///     POST /{organizationId}/calendar/{calendarId}/reservation
///     {
///         "name": "Board Meeting",
///         "startFrom": "2024-05-12T14:00:00",
///         "endAt": "2024-05-12T15:00:00",
///         "isWholeDay": false,
///         "status": "Confirmed",
///         "description": "Annual board meeting",
///         "cartId": "cart123",
///         "color": "blue",
///         "underName": "John Doe"
///     }
///
/// </remarks>
/// <response code="200">Returns the newly created reservation</response>
/// <response code="400">If the request body is null</response>
/// <response code="401">If the user is not authenticated</response>
/// <response code="404">If the specified organization does not exist</response>
[HttpPost("/{organizationId}/calendar/{calendarId}/reservation")]
[SwaggerOperation(Summary = "Create a new reservation", Description = "Creates a new reservation within a specified calendar and organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationCreateResponseModel), description : "Successfully created the reservation")]
[SwaggerResponse(statusCode: 400, description : "Bad request if the request body is null")]
[SwaggerResponse(statusCode: 401, description : "Unauthorized if the user is not logged in")]
[SwaggerResponse(statusCode: 404, description : "Not found if the specified organization or calendar does not exist")]
public async Task<ActionResult<ReservationCreateResponseModel>> CreateReservationAsync(
  [FromRoute] string organizationId, [FromRoute] string calendarId,
  [FromBody] ReservationCreateRequestModel request)
{
    if (request == null)
    {
        return BadRequest("Request cannot be null");
    }

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
    {
        return Unauthorized("User must be logged in to create a reservation");
    }

    var organization = _db.Organizations.Find(organizationId);
    if (organization == null)
    {
        return NotFound("Organization not found");
    }

    var reservation = new ReservationModel(Guid.NewGuid().ToString(),
        calendarId, organizationId,
        request.Name,
        request.StartFrom, request.EndAt, request.IsWholeDay, user.Id, request.Status)
    {
        Description = request.Description,
        CartId = request.CartId,
        Color = request.Color,
        UnderName = request.UnderName,
    };

    _db.Reservations.Add(reservation);
    await _db.SaveChangesAsync();

    var result = new ReservationCreateResponseModel(new ReservationViewModel()
    {
        Reservation = reservation
    });

    return Ok(result);
}

Read (Search) 検索

  /// <summary>
  /// Searches for reservations within a specific calendar and organization based on the provided search criteria.
  /// </summary>
  /// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
  /// <param name="calendarId">The ID of the calendar within which to search for reservations.</param>
  /// <param name="request">The search parameters including pagination details, optional keyword, and sort criteria.</param>
  /// <returns>A list of reservations that match the search criteria along with pagination details.</returns>
  /// <remarks>
  /// Sample request:
  ///
  ///     POST /{organizationId}/calendar/{calendarId}/reservation/search
  ///     {
  ///         "keyword": "meeting",
  ///         "sort": "name asc",
  ///         "currentPage": 1,
  ///         "itemsPerPage": 10
  ///     }
  ///
  /// </remarks>
  /// <response code="200">Returns the list of matching reservations along with pagination info</response>
  /// <response code="400">If the pagination parameters are invalid</response>
  [HttpPost("/{organizationId}/calendar/{calendarId}/reservation/search")]
  [SwaggerOperation(Summary = "Search reservations", Description = "Performs a search for reservations within a specified calendar and organization based on search criteria.")]
  [SwaggerResponse(statusCode: 200, type: typeof(ReservationsSearchResponseModel), description : "Successful retrieval of reservation list with pagination")]
  [SwaggerResponse(statusCode: 400, description : "Invalid request parameters")]
  public ActionResult<ReservationsSearchResponseModel> SearchReservation(
      [FromRoute] string organizationId, [FromRoute] string calendarId, [FromBody] ReservationsSearchRequestModel request)
  {
      // Input validation
      if (request.CurrentPage <= 0)
      {
          return BadRequest("CurrentPage must be greater than 0.");
      }
      if (request.ItemsPerPage <= 0)
      {
          return BadRequest("ItemsPerPage must be greater than 0.");
      }

      var reservations = from o in _db.Reservations
                         where o.OrganizationId == organizationId && o.CalendarId == calendarId
                         select o;

      if (!string.IsNullOrEmpty(request.Keyword))
      {
          reservations = reservations.Where(o => o.Name.Contains(request.Keyword));
      }

      // Count total items to support pagination
      int totalItems = reservations.Count();
      var items = reservations
                  .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                  .Take(request.ItemsPerPage)
                  .Select(r => new ReservationViewModel { Reservation = r })
                  .ToList();

      // Prepare the response model with pagination info
      var result = new ReservationsSearchResponseModel(items)
      {
          Keyword = request.Keyword,
          Sort = request.Sort,
          CurrentPage = request.CurrentPage,
          ItemsPerPage = request.ItemsPerPage,
          TotalItems = totalItems,
          TotalPages = (int)Math.Ceiling((double)totalItems / request.ItemsPerPage)
      };

      return Ok(result);
  }
        

Read (Get) 取得

/// <summary>
/// Retrieves a specific reservation by its ID within a given organization and calendar.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
/// <param name="calendarId">The ID of the calendar in which the reservation is scheduled.</param>
/// <param name="id">The unique identifier of the reservation to retrieve.</param>
/// <returns>Returns the detailed information of the reservation if found, or an error if not found.</returns>
/// <remarks>
/// Sample request:
///
///     GET /{organizationId}/calendar/{calendarId}/reservation/{id}
///
/// </remarks>
/// <response code="200">Returns the detailed information of the reservation</response>
/// <response code="404">If the reservation is not found within the specified organization and calendar</response>
[HttpGet("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
[SwaggerOperation(Summary = "Retrieve a reservation", Description = "Retrieves a specific reservation by its ID within a given organization and calendar.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationGetResponseModel), description : "Successfully retrieved the reservation")]
[SwaggerResponse(statusCode: 404, description : "Reservation not found")]
public ActionResult<ReservationGetResponseModel> GetReservation(
    [FromRoute] string organizationId, [FromRoute] string calendarId,
    [FromRoute] string id)
{
    // Query the database for the reservation with the specified ID and ensure it belongs to the specified organization and calendar
    var reservation = (from o in _db.Reservations
                       where o.Id == id && o.OrganizationId == organizationId &&
                             o.CalendarId == calendarId
                       select new ReservationViewModel
                       {
                           Reservation = o
                       }).FirstOrDefault();

    // Check if the reservation was found
    if (reservation == null)
    {
        return NotFound("The specified reservation was not found within the given organization and calendar.");
    }

    var result = new ReservationGetResponseModel(reservation);

    return Ok(result);
}

Update 更新

 /// <summary>
 /// Updates an existing reservation within a specified calendar and organization.
 /// </summary>
 /// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
 /// <param name="calendarId">The ID of the calendar in which the reservation is scheduled.</param>
 /// <param name="id">The unique identifier of the reservation to update.</param>
 /// <param name="request">The updated reservation details.</param>
 /// <returns>Returns the updated reservation details or appropriate error messages if the reservation cannot be found or updated.</returns>
 /// <remarks>
 /// Sample request:
 ///
 ///     PUT /{organizationId}/calendar/{calendarId}/reservation/{id}
 ///     {
 ///         "name": "Updated Meeting",
 ///         "description": "Updated description here",
 ///         "color": "green",
 ///         "bookerId": "user123",
 ///         "cartId": "cart456",
 ///         "deleted": false,
 ///         "endAt": "2024-05-20T15:00:00",
 ///         "isDeleted": false,
 ///         "isWholeDay": false,
 ///         "startFrom": "2024-05-20T14:00:00",
 ///         "status": "Confirmed",
 ///         "underName": "Jane Doe"
 ///     }
 ///
 /// </remarks>
 /// <response code="200">Successfully updated the reservation</response>
 /// <response code="404">If the reservation, organization, or calendar is not found</response>
 [HttpPut("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
 [SwaggerOperation(Summary = "Update a reservation", Description = "Updates an existing reservation within a specified calendar and organization.")]
 [SwaggerResponse(statusCode: 200, type: typeof(ReservationUpdateResponseModel), description : "Successfully updated the reservation")]
 [SwaggerResponse(statusCode: 404, description : "Not found if the specified reservation, organization, or calendar does not exist")]
 public async Task<ActionResult<ReservationUpdateResponseModel>> UpdateReservation(
     [FromRoute] string organizationId,
     [FromRoute] string calendarId,
     [FromRoute] string id, [FromBody] ReservationUpdateRequestModel request)
 {
     var original = await _db.Reservations.FindAsync(id);

     if (original == null)
     {
         return NotFound("The reservation with the specified ID was not found.");
     }

     if (original.OrganizationId != organizationId)
     {
         return NotFound("The reservation does not belong to the specified organization.");
     }

     if (original.CalendarId != calendarId)
     {
         return NotFound("The reservation does not belong to the specified calendar.");
     }

     original.Name = request.Name;
     original.Description = request.Description;
     original.Color = request.Color;
     original.BookerId = request.BookerId;
     original.CartId = request.CartId;
     original.Deleted = request.Deleted;
     original.EndAt = request.EndAt;
     original.IsDeleted = request.IsDeleted;
     original.IsWholeDay = request.IsWholeDay;
     original.StartFrom = request.StartFrom;
     original.Status = request.Status;
     original.UnderName = request.UnderName;

     _db.Reservations.Update(original);
     await _db.SaveChangesAsync();

     var result = new ReservationUpdateResponseModel(original);

     return Ok(result);
 }

Delete 削除

/// <summary>
/// Deletes a reservation within a specified calendar and organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
/// <param name="calendarId">The ID of the calendar from which the reservation is to be deleted.</param>
/// <param name="id">The unique identifier of the reservation to delete.</param>
/// <returns>Returns a confirmation of the deletion or an error message if the reservation cannot be found or is unauthorized.</returns>
/// <remarks>
/// This method deletes the reservation and returns the details of the deleted reservation.
/// </remarks>
/// <response code="200">Successfully deleted the reservation and returns the deleted reservation details</response>
/// <response code="404">If the reservation is not found within the specified organization and calendar</response>
/// <response code="401">If the reservation does not belong to the specified organization or calendar</response>
[HttpDelete("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
[SwaggerOperation(Summary = "Delete a reservation", Description = "Deletes a specific reservation within a specified calendar and organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationDeleteResponseModel), description : "Successfully deleted the reservation")]
[SwaggerResponse(statusCode: 404, description : "Not found if the specified reservation does not exist")]
[SwaggerResponse(statusCode: 401, description : "Unauthorized if the reservation does not belong to the specified organization or calendar")]
public async Task<ActionResult<ReservationDeleteResponseModel>> DeleteReservationAsync(
    [FromRoute] string organizationId, [FromRoute] string calendarId,
    [FromRoute] string id)
{
    var original = await _db.Reservations.FindAsync(id);

    if (original == null)
    {
        return NotFound("The reservation with the specified ID was not found.");
    }

    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The reservation does not belong to the specified organization.");
    }

    if (original.CalendarId != calendarId)
    {
        return Unauthorized("The reservation does not belong to the specified calendar.");
    }

    _db.Reservations.Remove(original);
    await _db.SaveChangesAsync();

    var result = new ReservationDeleteResponseModel(original);

    return Ok(result);
}

再度ここでSwaggerを確認。 OKそうなので、実際にコールしてみます。

uploading...0

カレンダーを作って表示してみる

では、早速作ったAPIを使ってカレンダーを表示してみましょう。

(local環境で動かしています)

ログイン

まずは、loginしてtokenを取得します。

リクエスト

curl -X 'POST' \
  'https://localhost:7135/login' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "username": "{username}",
  "password": "{password}"
}'

レスポンス

{
  "token": "{token}"
}

組織を作成

まだデータベースになにも入っていないので、組織から作ります。

リクエスト

curl -X 'POST' \
  'https://localhost:7135/organization' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "(株)レシートローラー"
}'

レスポンス

{
  "organization": {
    "id": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
    "name": "(株)レシートローラー",
    "created": "2024-05-12T20:21:17.4886117+09:00",
    "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
    "isSuspended": false,
    "suspended": "0001-01-01T00:00:00",
    "isDeleted": false,
    "deleted": "0001-01-01T00:00:00"
  },
  "organizationMembers": [
    {
      "id": "868e5cb1-4c9b-495e-9779-ad6b976eb85b",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "userId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "roleId": "admin"
    }
  ]
}

サポート用のカレンダーを作成

リクエスト

curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "レシートローラーアプリ導入サポートカレンダー",
  "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
  "timeZone": "JST",
  "isPublic": true,
  "defaultLocation": "Online",
  "maxAttendees": 100,
  "minAttendees": 1,
  "timeScale": 15
}'

レスポンス

{
  "calendar": {
    "calendar": {
      "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "レシートローラーアプリ導入サポートカレンダー",
      "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "timeZone": "JST",
      "color": null,
      "isPublic": true,
      "isDeleted": false,
      "defaultLocation": "Online",
      "maxAttendees": 100,
      "minAttendees": 1,
      "timeScale": 15,
      "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "created": "2024-05-12T11:30:12.6164258Z"
    }
  }
}

カレンダーを表示してみる

まだ予約はいれていないので、一旦カレンダー一覧を表示し、カレンダーの詳細を取得してみます。

リクエスト

curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/search' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "currentPage": 1,
  "itemsPerPage": 100
}'

レスポンス

{
  "keyword": null,
  "sort": null,
  "currentPage": 1,
  "itemsPerPage": 100,
  "totalItems": 1,
  "totalPages": 1,
  "items": [
    {
      "calendar": {
        "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
        "name": "レシートローラーアプリ導入サポートカレンダー",
        "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
        "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
        "timeZone": "JST",
        "color": null,
        "isPublic": true,
        "isDeleted": false,
        "defaultLocation": "Online",
        "maxAttendees": 100,
        "minAttendees": 1,
        "timeScale": 15,
        "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
        "created": "2024-05-12T11:30:12.6164258"
      },
      "numOfValidReservations": 0,
      "reservations": null
    }
  ]
}

単体で取得する際には reservationsを返すようにしています。こちらは実際に予約を入れた後に試してみます。

予約を入れてみる

では、予約を入れてみます。 予約時間の確認であったりとvalidationは追加するべきだとは思いますが、一旦はシンプルに予約のデータを書き込んでみます。

リクエスト

curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/7e826091-6147-43e5-aea2-7ee444b56eec/reservation' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "テスト予約",
  "description": "テストで予約してみます。",
  "startFrom": "2024-05-15T09:00:00",
  "endAt": "2024-05-15T09:15:00",
  "isWholeDay": false,
  "status": "New",
  "underName": "SHO SHIMODA"
}'

レスポンス

{
  "reservation": {
    "reservation": {
      "id": "b927571f-46be-44be-9071-ef121f8007e8",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "テスト予約",
      "description": "テストで予約してみます。",
      "color": null,
      "cartId": null,
      "startFrom": "2024-05-15T09:00:00",
      "endAt": "2024-05-15T09:15:00",
      "isWholeDay": false,
      "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "status": "New",
      "underName": "SHO SHIMODA",
      "created": "2024-05-12T21:22:20.0816013+09:00",
      "isDeleted": false,
      "deleted": "0001-01-01T00:00:00"
    }
  }
}

正常に予約ができました。 では、もう1個予約を入れてからカレンダーのデータをリクエストしてみます。

リクエスト

{
  "keyword": null,
  "sort": null,
  "currentPage": 1,
  "itemsPerPage": 100,
  "totalItems": 1,
  "totalPages": 1,
  "items": [
    {
      "calendar": {
        "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
        "name": "レシートローラーアプリ導入サポートカレンダー",
        "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
        "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
        "timeZone": "JST",
        "color": null,
        "isPublic": true,
        "isDeleted": false,
        "defaultLocation": "Online",
        "maxAttendees": 100,
        "minAttendees": 1,
        "timeScale": 15,
        "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
        "created": "2024-05-12T11:30:12.6164258"
      },
      "numOfValidReservations": 2,
      "reservations": null
    }
  ]
}

numOfValidReservation が正しく、2になっています。

次に詳細をリクエストしてみます。

リクエスト

curl -X 'GET' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/7e826091-6147-43e5-aea2-7ee444b56eec' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}'

レスポンス

{
  "calendar": {
    "calendar": {
      "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "レシートローラーアプリ導入サポートカレンダー",
      "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "timeZone": "JST",
      "color": null,
      "isPublic": true,
      "isDeleted": false,
      "defaultLocation": "Online",
      "maxAttendees": 100,
      "minAttendees": 1,
      "timeScale": 15,
      "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "created": "2024-05-12T11:30:12.6164258"
    },
    "numOfValidReservations": 2,
    "reservations": [
      {
        "reservation": {
          "id": "b927571f-46be-44be-9071-ef121f8007e8",
          "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
          "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
          "name": "テスト予約",
          "description": "テストで予約してみます。",
          "color": null,
          "cartId": null,
          "startFrom": "2024-05-15T09:00:00",
          "endAt": "2024-05-15T09:15:00",
          "isWholeDay": false,
          "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
          "status": "New",
          "underName": "SHO SHIMODA",
          "created": "2024-05-12T21:22:20.0816013",
          "isDeleted": false,
          "deleted": "0001-01-01T00:00:00"
        }
      },
      {
        "reservation": {
          "id": "7771e57d-b826-42fe-9949-57b47db4c5df",
          "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
          "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
          "name": "テスト予約2回目",
          "description": "テストで2回目予約してみます。",
          "color": null,
          "cartId": null,
          "startFrom": "2024-05-16T09:00:00",
          "endAt": "2024-05-16T09:15:00",
          "isWholeDay": false,
          "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
          "status": "New",
          "underName": "SHO SHIMODA",
          "created": "2024-05-12T21:23:46.3229625",
          "isDeleted": false,
          "deleted": "0001-01-01T00:00:00"
        }
      }
    ]
  }
}

まとめ

今日は一旦コアとなる部分を作成しました。ここからvalidationなど細かな処理であったりセキュリティー強化を追加して商品化していきます。

レシートローラー

レシートローラーは月々550円でLINEミニアプリ会員カード・電子レシート発行など、レジ周りのDXを推進しています。興味のある方は是非ホームページよりお問合せくださいませ。 https://receiptroller.com

本日書いたコードはこちらにありますのでご参照ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?