I have an ASP.NET Web API with Performer and Venue Models. I am attempting to create a Booking Model, wherein a Performer has many bookings, a Venue has many bookings, and a Booking has one Performer and one Venue.
I am using Swagger to send POST requests to create new Performers and Venues with no issues, by submitting JSON like the following in the request body:
{
"name": "Johnny Cash",
"description": "The Man in Black"
}
Performer.cs
public class Performer
{
public int Id { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
public ICollection<Booking> Bookings { get; } = new List<Booking>();
}
Venue.cs
public class Venue
{
public int Id { get; set; }
public required string Name { get; set; }
public required string Address { get; set; }
public ICollection<Booking> Bookings { get; } = new List<Booking>();
}
Booking.cs
public class Booking
{
public int Id { get; set; }
public DateTime BookingTime { get; set; }
public int PerformerId { get; set; }
public Performer Performer { get; set; } = null!;
public int VenueId { get; set; }
public Venue Venue { get; set; } = null!;
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public decimal PaymentAmount { get; set; }
public string AdditionalTerms { get; set; }
}
HttpPost from BookingsController.cs
[HttpPost]
public ActionResult<Booking> CreateBooking([FromBody] Booking booking)
{
_dbContext.Bookings.Add(booking);
_dbContext.SaveChanges();
return CreatedAtAction(nameof(GetBookingById), new { id = booking.Id }, booking);
}
When I try to submit a POST request with the following JSON...
{
"bookingTime": "2024-06-01T00:43:20.081Z",
"performerId": 54,
"venueId": 7,
"startTime": "2024-06-01T00:43:20.081Z",
"endTime": "2024-06-01T00:43:20.081Z",
"paymentAmount": 100,
"additionalTerms": "test 3"
}
I get a 400 Bad Request:
{
"errors": {
"Venue": [
"The Venue field is required."
],
"Performer": [
"The Performer field is required."
]
},
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-54b6a74cf601ebb713f0b9659fa724bc-502c5a31942dff72-00"
}
I try adding the corresponding Venue and Performer with values that match the data in my database, like so...
{
"bookingTime": "2024-06-01T01:00:32.279Z",
"performerId": 54,
"performer": {
"name": "Jonny Greenwood",
"description": "Sad and malnourished"
},
"venueId": 7,
"venue": {
"name": "Red Rocks",
"address": "Morrison, CO"
},
"startTime": "2024-06-01T01:00:32.279Z",
"endTime": "2024-06-01T01:00:32.279Z",
"paymentAmount": 100,
"additionalTerms": "test 3"
}
...which produces the following error, and also creates duplicate Performers and Venues as a sort of side effect:
Newtonsoft.Json.JsonSerializationException: Self referencing loop detected with type 'Booking'. Path 'performer.bookings'.
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value)
at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Accept: text/plain
Connection: keep-alive
Host: localhost:5160
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Content-Type: application/json-patch+json
Cookie: .AspNetCore.Antiforgery.0euMeX0wUno=CfDJ8Fks6S2gTq9AikuUf-uH-kpTqt8HqFTItQdu_ivLxVrTJwOF6Ulr4W-j5NwOy8-iZkjX8Pev7wN5SIrD05kPq9jspuEaz7AqGUVdn8u0EAWXtrYyChEDhnqs5MXUXNPjI7L8HlsL-xdrb6Pq5aeKWSw
Origin: http://localhost:5160
Referer: http://localhost:5160/swagger/index.html
Content-Length: 391
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Lastly, when I try to add the corresponding Ids for Performers and Venues...
{
"bookingTime": "2024-06-01T01:00:32.279Z",
"performerId": 54,
"performer": {
"id": 54,
"name": "Jonny Greenwood",
"description": "Sad and malnourished"
},
"venueId": 7,
"venue": {
"id": 7,
"name": "Red Rocks",
"address": "Morrison, CO"
},
"startTime": "2024-06-01T01:00:32.279Z",
"endTime": "2024-06-01T01:00:32.279Z",
"paymentAmount": 100,
"additionalTerms": "test 3"
}
...I get a different error:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
---> Npgsql.PostgresException (0x80004005): 23505: duplicate key value violates unique constraint "PK_Performers"
DETAIL: Detail redacted as it may contain sensitive data. Specify 'Include Error Detail' in the connection string to include this information.
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult()
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior)
at Npgsql.NpgsqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
Exception data:
Severity: ERROR
SqlState: 23505
MessageText: duplicate key value violates unique constraint "PK_Performers"
Detail: Detail redacted as it may contain sensitive data. Specify 'Include Error Detail' in the connection string to include this information.
SchemaName: public
TableName: Performers
ConstraintName: PK_Performers
File: nbtinsert.c
Line: 673
Routine: _bt_check_unique
--- End of inner exception stack trace ---
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__112_0(DbContext _, ValueTuple`2 t)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
at BookingsController.CreateBooking(Booking booking) in /Users/tyleracosta/Desktop/Projects/booking-backend/BookingApi/Controllers/BookingsController.cs:line 40
at lambda_method129(Closure, Object, Object[])
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Accept: text/plain
Connection: keep-alive
Host: localhost:5160
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Content-Type: application/json-patch+json
Cookie: .AspNetCore.Antiforgery.0euMeX0wUno=CfDJ8Fks6S2gTq9AikuUf-uH-kpTqt8HqFTItQdu_ivLxVrTJwOF6Ulr4W-j5NwOy8-iZkjX8Pev7wN5SIrD05kPq9jspuEaz7AqGUVdn8u0EAWXtrYyChEDhnqs5MXUXNPjI7L8HlsL-xdrb6Pq5aeKWSw
Origin: http://localhost:5160
Referer: http://localhost:5160/swagger/index.html
Content-Length: 418
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Is it possible to submit a POST request to create a new Booking just by using JSON? Or do I have to use a DTO or service?
Is it possible to submit a POST request to create a new Booking just by using JSON? Or do I have to use a DTO or service?
You'll still be working/sending JSON to your API regardless of whether you use the Booking
entity or a DTO
(search 'route model binding').
Your initial issue (400 Bad Request
) is result of using your Booking
entity directly which has the Venue
and Performer
properties which are not nullable
and therefore required but not being provided in your JSON
request.
It is not recommended to use entities directly for your API request and response objects as they can leak domain logic (amongst other things) which is related to the issue(s) you're experiencing.
It would be advisable to create request and response specific objects to encapsulate the required state. For example:
public record CreateBookingRequest
{
public int PerformerId { get; init; }
public int VenueId { get; init; }
// Other properties required for creating a booking
}