I'm creating an ASP.NET Web API to perform CRUD operations in a MongoDB database. I've been able to create a simple application based on the following Microsoft tutorial: Create a web API with ASP.NET Core and MongoDB.
This tutorial, like others I found, all use defined data models (in the above tutorial it's the Book model). In my case i need to perform CRUD operations with generic JSON objects. For example, the JSON object might be any of the following examples:
Example #1:
{_id: 1, name: 'Jon Snow', address: 'Castle Black', hobbies: 'Killing White Walkers'}
Example #2:
{_id: 2, name: 'Daenerys Targaryen', family: 'House Targaryen', titles: ['Queen of Meereen', 'Khaleesi of the Great Grass Sea', 'Mother of Dragons', 'The Unburnt', 'Breaker of Chains', 'Queen of the Andals and the First Men', 'Protector of the Seven Kingdoms', 'Lady of Dragonstone']}
The reason why I'm using a NoSQL database (MongoDB) is mainly because of the undefined data structure, and the ability to preform CRUD operations with just JSON.
As a trial and error attempt, I replaced the 'Book' model with 'object' and 'dynamic' but I get all sorts of errors regarding cast types and unknown properties:
public class BookService
{
private readonly IMongoCollection<object> _books;
public BookService(IBookstoreDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);
_books = database.GetCollection<object>(settings.BooksCollectionName);
}
public List<object> Get() => _books.Find(book => true).ToList();
//public object Get(string id) => _books.Find<object>(book => book.Id == id).FirstOrDefault();
//public object Create(object book)
//{
// _books.InsertOne(book);
// return book;
//}
//public void Update(string id, object bookIn) => _books.ReplaceOne(book => book.Id == id, bookIn);
//public void Remove(object bookIn) => _books.DeleteOne(book => book.Id == bookIn.Id);
//public void Remove(string id) => _books.DeleteOne(book => book.Id == id);
}
Errors:
'object' does not contain a definition for 'Id' and no accessible extension method 'Id' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?)
InvalidCastException: Unable to cast object of type 'd__51' to type 'System.Collections.IDictionaryEnumerator'.
So, my question is, how can I use generic JSON data types with ASP.NET Core Web API and the MongoDB Driver?
UPDATE: Based on @pete-garafano suggestion, I've decided to proceed with a POCO model.
I found an article in MongoDB's Github page explaining how to use static and dynamic data with the ASP.NET Core Driver. So I made the following changes to the Book model:
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public string Author { get; set; }
[BsonExtraElements]
public BsonDocument Metadata { get; set; } //new property
}
Now I'm facing other issues, if my data is formatted exactly as the model, i'm able to list the data and create new entries in the database. But, if i try to create a new entry with the bellow format, i get an error:
{
"Name": "test 5",
"Price": 19,
"Category": "Computers",
"Author": "Ricky",
"Metadata": {"Key": "Value"} //not working with this new field
}
System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonElement' to type 'MongoDB.Bson.BsonDocument'.
Also, if i change the data format of one entry in Mongo and then try to list all results, i get the same error:
System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonDocument' to type 'MongoDB.Bson.BsonBoolean'.
Based on the Mongo documents, the BsonExtraElements should allow generic/dynamic data to be attached to the model. What am I doing wrong in the new approach?
UPDATE #2: Added detailed stack trace of the error
System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonDocument' to type 'MongoDB.Bson.BsonBoolean'. at get_AsBoolean(Object ) at System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer) at System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer) at System.Text.Json.JsonSerializer.HandleObject(JsonPropertyInfo jsonPropertyInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) at System.Text.Json.JsonSerializer.WriteObject(JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.JsonSerializer.WriteAsyncCore(Stream utf8Json, Object value, Type inputType, JsonSerializerOptions options, CancellationToken cancellationToken) at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|29_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 where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
UPDATE #3: Added the Book service and controller code files, database Book collection and exception launched in get() result.
BookServices.cs:
public class BookService
{
private readonly IMongoCollection<Book> _books;
public BookService(IBookstoreDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);
_books = database.GetCollection<Book>(settings.BooksCollectionName);
}
public List<Book> Get() => _books.Find(book => true).ToList();
public Book Get(string id) => _books.Find<Book>(book => book.Id == id).FirstOrDefault();
public Book Create(Book book)
{
_books.InsertOne(book);
return book;
}
public void Update(string id, Book bookIn) => _books.ReplaceOne(book => book.Id == id, bookIn);
public void Remove(Book bookIn) => _books.DeleteOne(book => book.Id == bookIn.Id);
public void Remove(string id) => _books.DeleteOne(book => book.Id == id);
}
BooksController.cs:
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
private readonly BookService _bookService;
public BooksController(BookService bookService)
{
_bookService = bookService;
}
[HttpGet]
public ActionResult<List<Book>> Get() => _bookService.Get(); // error happens when executing Get()
[HttpGet("{id:length(24)}", Name = "GetBook")]
public ActionResult<Book> Get(string id)
{
var book = _bookService.Get(id);
if (book == null)
{
return NotFound();
}
return book;
}
[HttpPost]
public ActionResult<Book> Create([FromBody] Book book)
{
_bookService.Create(book);
return CreatedAtRoute("GetBook", new { id = book.Id.ToString() }, book);
}
[HttpPut("{id:length(24)}")]
public IActionResult Update(string id, Book bookIn)
{
var book = _bookService.Get(id);
if (book == null)
{
return NotFound();
}
_bookService.Update(id, bookIn);
return NoContent();
}
[HttpDelete("{id:length(24)}")]
public IActionResult Delete(string id)
{
var book = _bookService.Get(id);
if (book == null)
{
return NotFound();
}
_bookService.Remove(book.Id);
return NoContent();
}
}
BookstoreDb.Books:
//non-pretty
{ "_id" : ObjectId("5df2b193405b7e9c1efa286f"), "Name" : "Design Patterns", "Price" : 54.93, "Category" : "Computers", "Author" : "Ralph Johnson" }
{ "_id" : ObjectId("5df2b193405b7e9c1efa2870"), "Name" : "Clean Code", "Price" : 43.15, "Category" : "Computers", "Author" : "Robert C. Martin" }
{ "_id" : ObjectId("5df2b1c9fe91da06078d9fbb"), "Name" : "A New Test", "Price" : 43.15, "Category" : "Computers", "Author" : "Ricky", "Metadata" : { "Key" : "Value" } }
Detailed result from Mongo Driver:
[/0]:{Api.Models.Book} Author [string]:"Ralph Johnson" Category [string]:"Computers" Id [string]:"5df2b193405b7e9c1efa286f" Metadata [BsonDocument]:null Name [string]:"Design Patterns" Price [decimal]:54.93
[/1]:{Api.Models.Book} Author [string]:"Robert C. Martin" Category [string]:"Computers" Id [string]:"5df2b193405b7e9c1efa2870" Metadata [BsonDocument]:null Name [string]:"Clean Code" Price [decimal]:43.15
[/2]:{Api.Models.Book} Author [string]:"Ricky" Category [string]:"Computers" Id [string]:"5df2b1c9fe91da06078d9fbb" Metadata [BsonDocument]:{{ "Metadata" : { "Key" : "Value" } }} AllowDuplicateNames [bool]:false AsBoolean [bool]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBoolean' threw an exception of type 'System.InvalidCastException' AsBsonArray [BsonArray]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonArray' threw an exception of type 'System.InvalidCastException' AsBsonBinaryData [BsonBinaryData]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonBinaryData' threw an exception of type 'System.InvalidCastException' AsBsonDateTime [BsonDateTime]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonDateTime' threw an exception of type 'System.InvalidCastException' AsBsonDocument [BsonDocument]:{{ "Metadata" : { "Key" : "Value" } }} AsBsonJavaScript [BsonJavaScript]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonJavaScript' threw an exception of type 'System.InvalidCastException' AsBsonJavaScriptWithScope [BsonJavaScriptWithScope]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonJavaScriptWithScope' threw an exception of type 'System.InvalidCastException' AsBsonMaxKey [BsonMaxKey]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonMaxKey' threw an exception of type 'System.InvalidCastException' AsBsonMinKey [BsonMinKey]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonMinKey' threw an exception of type 'System.InvalidCastException' AsBsonNull [BsonNull]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonNull' threw an exception of type 'System.InvalidCastException' AsBsonRegularExpression [BsonRegularExpression]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonRegularExpression' threw an exception of type 'System.InvalidCastException' AsBsonSymbol [BsonSymbol]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonSymbol' threw an exception of type 'System.InvalidCastException' AsBsonTimestamp [BsonTimestamp]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonTimestamp' threw an exception of type 'System.InvalidCastException' AsBsonUndefined [BsonUndefined]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsBsonUndefined' threw an exception of type 'System.InvalidCastException' AsBsonValue [BsonValue]:{{ "Metadata" : { "Key" : "Value" } }} AsByteArray [byte[]]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsByteArray' threw an exception of type 'System.InvalidCastException' AsDateTime [DateTime]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsDateTime' threw an exception of type 'System.InvalidCastException' AsDecimal [decimal]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsDecimal' threw an exception of type 'System.InvalidCastException' AsDecimal128 [Decimal128]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsDecimal128' threw an exception of type 'System.InvalidCastException' AsDouble [double]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsDouble' threw an exception of type 'System.InvalidCastException' AsGuid [Guid]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsGuid' threw an exception of type 'System.InvalidCastException' AsInt32 [int]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsInt32' threw an exception of type 'System.InvalidCastException' AsInt64 [long]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsInt64' threw an exception of type 'System.InvalidCastException' AsLocalTime [DateTime]:'(new System.Collections.Generic.ICollectionDebugView(test).Items2).Metadata.AsLocalTime' threw an exception of type 'System.InvalidCastException' [More] Name [string]:"A New Test" Price [decimal]:43.15


