使用通用 JSON 对象的 .NET Core Web API 和 MongoDB 驱动程序

Ric*_*cky 4 c# mongodb-.net-driver asp.net-core-webapi

我正在创建一个 ASP.NET Web API 来在 MongoDB 数据库中执行 CRUD 操作。我已经能够根据以下 Microsoft 教程创建一个简单的应用程序:使用 ASP.NET Core 和 MongoDB 创建 Web API

本教程和我发现的其他教程一样,都使用定义的数据模型(在上面的教程中是 Book 模型)。就我而言,我需要使用通用 JSON 对象执行 CRUD 操作。例如,JSON 对象可能是以下任意示例:

示例#1:

{_id: 1, name: 'Jon Snow', address: 'Castle Black', hobbies: 'Killing White Walkers'}
Run Code Online (Sandbox Code Playgroud)

示例#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']}
Run Code Online (Sandbox Code Playgroud)

我使用 NoSQL 数据库(MongoDB)的原因主要是因为未定义的数据结构,以及仅使用 JSON 执行 CRUD 操作的能力。

作为尝试和错误的尝试,我将“Book”模型替换为“object”和“dynamic”,但我收到了有关强制转换类型和未知属性的各种错误:

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);
}
Run Code Online (Sandbox Code Playgroud)

错误:

“object”不包含“Id”的定义,并且找不到接受“object”类型的第一个参数的可访问扩展方法“Id”(您是否缺少 using 指令或程序集引用?)

InvalidCastException:无法将类型“d__51”的对象转换为类型“System.Collections.IDictionaryEnumerator”。

所以,我的问题是,如何将通用 JSON 数据类型与 ASP.NET Core Web API 和 MongoDB 驱动程序一起使用?

更新:根据 @pete-garafano 的建议,我决定继续使用 POCO 模型。

在 MongoDB 的 Github页面中找到了一篇文章,解释了如何通过 ASP.NET Core 驱动程序使用静态和动态数据。所以我对 Book 模型做了以下修改:

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
}
Run Code Online (Sandbox Code Playgroud)

现在我面临其他问题,如果我的数据的格式与模型完全相同,我就可以列出数据并在数据库中创建新条目。但是,如果我尝试使用以下格式创建新条目,则会收到错误:

{
    "Name": "test 5",
    "Price": 19,
    "Category": "Computers",
    "Author": "Ricky",
    "Metadata": {"Key": "Value"} //not working with this new field
}
Run Code Online (Sandbox Code Playgroud)

System.InvalidCastException:无法将类型“MongoDB.Bson.BsonElement”的对象转换为类型“MongoDB.Bson.BsonDocument”。

另外,如果我更改 Mongo 中一个条目的数据格式,然后尝试列出所有结果,我会收到相同的错误:

Mongo Compass 文档列表

System.InvalidCastException:无法将类型“MongoDB.Bson.BsonDocument”的对象转换为类型“MongoDB.Bson.BsonBoolean”。

根据Mongo 文档,BsonExtraElements 应该允许将通用/动态数据附加到模型。我在新方法中做错了什么?

更新#2:添加了错误的详细堆栈跟踪

System.InvalidCastException:无法将类型“MongoDB.Bson.BsonDocument”的对象转换为类型“MongoDB.Bson.BsonBoolean”。在 get_AsBoolean(Object ) 在 System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer) 在 System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer) 在 System.Text.Json.JsonSerializer。 HandleObject(JsonPropertyInfo jsonPropertyInfo,JsonSerializerOptions选项,Utf8JsonWriter编写器,WriteStack&状态)在System.Text.Json.JsonSerializer.WriteObject(JsonSerializerOptions选项,Utf8JsonWriter编写器,WriteStack&状态)在System.Text.Json.JsonSerializer.Write(Utf8JsonWriter编写器,Int32原始WriterDepth 、Int32lushThreshold、JsonSerializerOptions 选项、WriteStack& 状态)位于 System.Text.Json.JsonSerializer.WriteAsyncCore(Stream utf8Json、对象值、类型 inputType、JsonSerializerOptions 选项、CancellationToken CancellationToken)位于 Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext上下文,编码 selectedEncoding) 在 Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext 上下文,编码 selectedEncoding) 在 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker.g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker 调用程序,任务 lastTask,状态下一个、范围范围、对象状态、布尔值 isCompleted) 在 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker.Rethrow(ResultExecutedContextSealed context) 在 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope&范围、对象和状态、布尔值和 isCompleted)位于 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker.InvokeResultFilters() --- 抛出异常的上一个位置的堆栈跟踪结束 --- 位于 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker。 g__Awaited|19_0(ResourceInvoker 调用程序、任务lastTask、状态下一个、范围范围、对象状态、布尔值 isCompleted)位于 Microsoft.AspNetCore.Mvc.Infrastruct.ResourceInvoker.g__Logged|17_1(ResourceInvoker 调用程序)位于 Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask |6_0(终结点端点、任务 requestTask、ILogger 记录器)位于 Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext 上下文)位于 Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext 上下文)

更新#3:添加了图书服务和控制器代码文件、数据库图书集合以及在 get() 结果中启动的异常。

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);
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

书店Db.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" } }
Run Code Online (Sandbox Code Playgroud)

Mongo 驱动程序的详细结果:

[/0]:{Api.Models.Book} 作者 [字符串]:"Ralph Johnson" 类别 [字符串]:"计算机" Id [字符串]:"5df2b193405b7e9c1efa286f" 元数据 [BsonDocument]:null 名称 [字符串]:"设计图案”价格[十进制]:54.93

[/1]:{Api.Models.Book} 作者 [字符串]:"Robert C. Martin" 类别 [字符串]:"计算机" Id [字符串]:"5df2b193405b7e9c1efa2870" 元数据 [BsonDocument]:null 名称 [字符串]: “干净的代码”价格[十进制]:43.15

[/2]:{Api.Models.Book} 作者 [string]:"Ricky" 类别 [string]:"Computers" Id [string]:"5df2b1c9fe91da06078d9fbb" 元数据 [BsonDocument]:{{ "Metadata" : { "Key " : "Value" } }} AllowDuplicateNames [bool]:false AsBoolean [bool]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBoolean' 抛出类型为 'System.InvalidCastException 的异常' AsBsonArray [BsonArray]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonArray' 抛出类型为 'System.InvalidCastException' 的异常 AsBsonBinaryData [BsonBinaryData]:'(new System.Collections. Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonBinaryData' 抛出类型为 'System.InvalidCastException' AsBsonDateTime [BsonDateTime]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata的异常。 AsBsonDateTime' 抛出类型为 'System.InvalidCastException' 的异常 AsBsonDocument [BsonDocument]:{{ "Metadata" : { "Key" : "Value" } }} AsBsonJavaScript [BsonJavaScript]:'(new System.Collections.Generic.ICollectionDebugView( test).Items 2 ).Metadata.AsBsonJavaScript' 引发了类型为 'System.InvalidCastException' AsBsonJavaScriptWithScope [BsonJavaScriptWithScope]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonJavaScriptWithScope' 引发了'System.InvalidCastException' AsBsonMaxKey [BsonMaxKey]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonMaxKey' 类型的异常引发了 'System.InvalidCastException' AsBsonMinKey [BsonMinKey] 类型的异常: '(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonMinKey' 抛出类型为 'System.InvalidCastException' AsBsonNull [BsonNull]:'(new System.Collections.Generic.ICollectionDebugView(test) .Items 2 ).Metadata.AsBsonNull' 引发了类型为 'System.InvalidCastException' AsBsonRegularExpression [BsonRegularExpression]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonRegularExpression' 引发的异常类型 'System.InvalidCastException' AsBsonSymbol [BsonSymbol]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonSymbol' 抛出类型 'System.InvalidCastException' AsBsonTimestamp [BsonTimestamp]:'(新的系统.集合。Generic.ICollectionDebugView(测试).Items 2 ).Metadata.AsBsonTimestamp' 抛出类型为 'System.InvalidCastException' AsBsonUndefined [BsonUndefined]:'(新的系统.集合。Generic.ICollectionDebugView(测试).Items 2).Metadata.AsBsonUndefined' 引发了类型为 'System.InvalidCastException' 的异常 AsBsonValue [BsonValue]:{{ "Metadata" : { "Key" : "Value" } }} AsByteArray [byte[]]:'(new System. Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsByteArray' 抛出类型为 'System.InvalidCastException' AsDateTime [DateTime]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 )的异常。 Metadata.AsDateTime' 引发了“System.InvalidCastException”类型的异常 AsDecimal [decimal]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsDecimal' 引发了“System.InvalidCastException”类型的异常' AsDecimal128 [Decimal128]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsDecimal128' 引发了类型为 'System.InvalidCastException' 的异常 AsDouble [double]:'(new System.Collections. Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsDouble' 抛出类型为 'System.InvalidCastException' AsGuid [Guid]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata 的异常。 AsGuid' 抛出了类型为 'System.InvalidCastException' AsInt32 [int]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsInt32' 抛出了类型为 'System.InvalidCastException' AsInt64 的异常[long]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsInt64' 抛出类型为 'System.InvalidCastException' AsLocalTime [DateTime]:'(new System.Collections.Generic. ICollectionDebugView(test).Items 2 ).Metadata.AsLocalTime' 抛出类型为 'System.InvalidCastException' 的异常 [更多] 名称 [字符串]:“新测试” 价格 [十进制]:43.15

Pet*_*ano 5

您应该使用BsonDocumentC# 和 MongoDB 来处理非类型化数据。

private readonly IMongoCollection<BsonDocument> _books;
Run Code Online (Sandbox Code Playgroud)

这并不理想,因为 C# 更喜欢强类型字段名称。我建议尝试为数据构建 POCO 模型以简化查询/更新操作。如果你不能做到这一点,你将无法使用类似的语法

_books.DeleteOne(book => book.Id == id);
Run Code Online (Sandbox Code Playgroud)

您将需要使用字典类型访问器语法,例如:

_books.DeleteOne(book => book["_id"] == id);
Run Code Online (Sandbox Code Playgroud)

请注意,该_id字段在 MongoDB 中很特殊,因为它必须出现在每个文档中,并且在集合中是唯一的。在您链接到的示例中,它们提供了一个实体模型。该模型中的字段Id有 2 个装饰器

[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
Run Code Online (Sandbox Code Playgroud)

这些告诉驱动程序该Id字段应该用作_id, 并且该字段,而 C# 中的字符串应该被ObjectIdMongoDB 视为 an 。

如果您使用的是完全无类型的模型,您将需要了解 和 之间的区别_idid并确保正确映射字段或创建索引id(前者可能是您想要的)。

我前段时间写了一篇文章,也许对你有帮助。它涵盖了与 Microsoft 帖子大部分相同的材料,但可能会为您提供更多见解。

虽然您提供的示例数据确实有所不同,但仍然可以创建一个 POCO 模型,允许您在查询中使用类型信息。我建议您研究一下这样做的可行性,以简化您的开发。正如我上面所解释的,这不是必需的,但它肯定会改善查询体验。

更新以解决额外问题

BsonExtraElements属性是驱动程序反序列化模型中不存在的字段的地方。例如,如果您将该Metadata字段重命名为,例如,,Foo然后重新运行它。Metadata数据库中的字段现在实际上应该包含在该字段中Foo

System.InvalidCastException:无法将类型“MongoDB.Bson.BsonDocument”的对象转换为类型“MongoDB.Bson.BsonBoolean”。

此异常似乎表明BsonDocument数据库中存在某些内容,但驱动程序正在尝试将其分配给一个bool值。我无法重现该错误。我在数据库中创建了一个文档,就像您上面提供的那样。 数据库文件

然后我使用 LINQPad 和一个简单的程序进行查询。 示例程序

您能否提供其余的堆栈跟踪?它可以为我们提供有关哪个字段导致问题的更多信息。您还可以尝试BsonExtraElementsMetadataPOCO 中删除 并仅为BsonExtraElements.

更新3

感谢您提供完整的堆栈跟踪。这让我“啊哈!” 片刻。该错误本身并非来自 MongoDB 驱动程序。该错误实际上来自 JSON 序列化器,因为它访问BsonDocument类型上的所有字段。

ABsonDocument是一种懒惰型。在您尝试访问它之前,它并不“知道”它包含什么。这是通过为许多不同字段提供 getter 来处理的,所有字段均按其可能包含的类型命名。您可以在这里看到它们。

ASP 中的 JSON 序列化程序尽职尽责地迭代每个字段(AsBooleanAsBsonArrayAsBsonBinaryData等),尝试检索要序列化为 JSON 的值。不幸的是,其中大多数都会失败,因为 in 的值Metadata无法投射到其中的大多数(或任何)。

认为您需要告诉 JSON 序列化器忽略字段Metadata,或者为BsonDocument.