In this video, learn how to create an action method that handles a POST request to the API to create a new resource.
- [Instructor] Before we dive into forms and modifying data, I should mention that I updated the Exercise Files with my solution to the challenge I posed at the end of the previous chapter. You can find the updated code in the Begin state of the Exercise Files for this video. If you've been following along throughout this course, I recommend opening the solution file from the Exercise Files and continuing from there, so we both have the same changes. Okay, let's build a way for a client to create a booking for a room in the hotel. I'll start on the RoomsController.
This will be a brand new route on the RoomsController called CreateBookingForRoom. So it'll be public async Task. For now, we'll just return an ActionResult. We'll call it CreateBookingForRoom and it'll accept a roomId. The route is here is gonna be an HttpPost to roomId /bookings, which means the full action will be post /rooms/ some roomId/bookings.
Let's name this as well, we'll say name equals nameOf CreateBook... Book? BookingForRoom. There we go, CreateBookingForRoom. Right now, we'll just have this throw an exception and we'll come back to the implementation of this. The JSON body that the client will post to this route will be defined in a new class called BookingForm. I'll create that in the Models folder.
Called BookingForm. And a BookingForm will have two properties, a DateTimeOffset called StartAt and a DateTimeOffset called EndAt. I'm making these nullable, so if the client doesn't include them or forgets to include them, they'll come over as null. I want ASP.net Core to do the validation of these for me automatically, so I'll add some attributes here. I'll say this is required.
And I can also say, Display and say the name of this is startAt. And the description is Booking start time. This will make any validation error messages a little bit nicer for the client. Duplicate this on the other one and call this EndAt, Booking end time. Back in the RoomsController, I need to add this to the parameters for this method. So I'll say that this method also accepts a BookingForm called BookingForm.
And we need to mark this as being FromBody, so ASP.net Core knows to bind it from the body of the post request. In this method itself, we can do some business logic first. We'll assume that the parameters on BookingForm have been validated and they're not null because ASP.net Core is gonna do model validation for us automatically. The first thing we need to do is look up the room for this booking. We'll do that from the roomService. GetRoom Async, and pass the roomId.
Now, if this room doesn't exist for some reason, the ID was invalid or wrong, we can say if the room is null, we'll just return NotFound 404. And for the sake of being complete here, I'll add ProducesResponseType (404) for any introspection that happens at some later time in our API. Now, we need to validate that the length of stay that this booking represents is not less than the minimum stay for the hotel. I'll use the dateLogicService to do this for me.
I added a reference to the dateLogicService in this controller earlier. If you don't have it in your controller, you'll need to inject it in the constructor. You can say minimumStay equals the dateLogicService.getMinimumStay. And then I'll just do a quick check. I'll say it's tooShort if the bookingForm.StartAt... Actually, the EndAt value. If the EndAt value minus the bookingForm .StartAt value, if that is less than the minimumStay.
If we're too short, I just wanna return a nice error message to the client, so we'll say return BadRequest or HTTP 400, with a new ApiError. We'll say, The minimum, maybe, booking duration is minimumStay .totalHours, hours.
I need to go add a small overload to ApiError that accepts a string. Let's try ApiError, string message. And we'll just copy that message into the message property. We also need to check that this booking doesn't conflict with an existing booking for the same room at the same time in this hotel. We'll pull out a list of conflictedSlots from the openingService, given this roomId, the start time, and the end time.
If there are any conflicts, we'll also return a BadRequest. We'll say something like, This time conflicts with an existing booking. If you're curious how I implemented this check, look at the logic in the getConflictingSlots method. Now, we need a userId for the user who's booking this room in the hotel. We haven't identity or authentication to the API yet.
So for now, I'll just cheat and say, the userId is just a NewGuid. We will need a way to get the current user there later on. And just to remind myself, I'll put a note at the top of this and say, TODO, we need authentication here. Okay, we're ready to create a booking. Let's do bookingId for the new booking is calling the bookingService. CreateBookingAsync.
Passing the userId, passing the roomId, and the start and end times. And at the end of this method, we'll return Created or HTTP 201, with a link to the location where we can retrieve the new booking, so that'll be Url.Link, name of the BookingsController .GetBookingById, with the parameter of bookingId.
And we don't need any value for the last parameter of Created. Since we're returning HTTP 201 here, I'll add one more thing, ProducesResponseType (201). And we actually return some 400, so I'll add that one too. I don't have a reference to the BookingService yet, so let me add that at the top. Say IBookingService, bookingService. And inject it in the constructor.
Okay, let's try out what we have so far. To test this out in Postman, I need to switch to a post request. I'm gonna send this to /rooms/, I'll grab one of these rooms from my history here, and /bookings. And in the body of the request in Postman, I need to switch to Raw and then application/json to send a JSON post. Let me try just sending it with an empty body. This should produce an error.
If the client sends the wrong parameters or is missing some parameters, they get a nice error message back which tells them exactly what's wrong. So let's try adding the EndAt field. This is an ISO date, so we can say, 2018, 07, 30, for example. Let's try this again. It should be complain about StartAt now. Let's try adding StartAt. 07-29. That conflicts with an existing booking.
Okay, let's try 08- 02 to 08- 03. We got another error here, but that's because we haven't implemented the CreateBookingMethod on the bookingService. The validation passed, in this case. Let's go ahead and implement that service method. Over in the BookingService, DefaultBookingService, in the CreateBookingAsync method, we have a stub here, so let's go ahead and replace this.
First thing we need to do here is pull out the room, so we'll do context.Rooms, .SingleOrDefaultAsync, where the roomId equals this roomId. We need to make this an async method here to use await. Now, this shouldn't happen, but if for some reason it's null we'll return an error. We'll just throw an argument exception and say, this is an invalid room ID.
Now, let's create a new booking model. We'll say newBooking equals context.Bookings.Add, new BookingEntity, where the ID is just a NewGuid. CreatedAt and ModifiedAt are the current time. The StartAt value is what's requested in the booking, so we'll say StartAt, convert this to Universal Time.
EndAt is the same. EndAt.ToUniversalTime. We need the total cost, which we haven't calculate quite yet and then room is the room that's requested here. To calculate the total cost, we need to multiply the room rate by how many days or really how many minimum stays this booking represents. So we'll say the minimumStay for the hotel comes from the dateLogicService.
GetMinimumStay. And we'll calculate the total by saying, it's an int. We will calculate EndAt minus StartAt, the total hours of that, divided by the minimumStay's total hours, and we'll multiply all of that by room.Rate. We're casting to an int at the end here because room.Rate is stored as an int in cents.
Now, at the bottom, we can save this. So we can say, created equals await, context.SaveChangesAsync. SaveChangesAsync will return an int, representing how many items or entities in the database were affected by this save operation. If we affected less than one item, then we know something went wrong and we didn't save. We can say if created is less than one, then we need to throw an exception. We'll say InvalidOperationException, Could not create the booking.
Otherwise, we'll just return the ID of the new booking. Let's pull this out and say, var id equals Guid. Put the id here. Okay, let's try creating a booking again. We'll send the same request via Postman and try to create a booking in the hotel. We got a 200 OK response, or actually, 201 Created, and no body, but in the location header, we have a location to the newly created resource.
So let's go retrieve that. And now, we can see the new booking that we created in the hotel. Now that clients can create a booking resource, we should also give them a way to delete or cancel that booking as well. We'll implement the delete verb next.
- What is RESTful design?
- Building a new API with ASP.NET Core
- Using HTTP methods
- Returning JSON
- Creating RESTful routing with templates
- Securing RESTful APIs with HTTPS
- Representing resources
- Representing links
- Representing collections
- Sorting and searching collections
- Building forms
- Adding caching to an ASP.NET Core API
- Configuring user authentication and authorization