首页 > 解决方案 > .NET Core Web API 和 MongoDB 驱动程序使用通用 JSON 对象

问题描述

我正在创建一个 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'}

示例 #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']}

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

作为一次试错尝试,我用 'object' 和 'dynamic' 替换了 'Book' 模型,但我得到了关于强制转换类型和未知属性的各种错误:

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);
}

错误:

“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
}

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

{
    "Name": "test 5",
    "Price": 19,
    "Category": "Computers",
    "Author": "Ricky",
    "Metadata": {"Key": "Value"} //not working with this new field
}

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”类型。在 System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer) 在 System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer) 在 System.Text.Json.JsonSerializer 的 get_AsBoolean(Object)。 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 选项, WriteStack&

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

图书服务.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" } }

Mongo Driver的详细结果:

[/0]:{Api.Models.Book} 作者 [string]:"Ralph Johnson" 类别 [string]:"Computers" Id [string]:"5df2b193405b7e9c1efa286f" 元数据 [BsonDocument]:null Name [string]:"Design图案”价格[十进制]:54.93

[/1]:{Api.Models.Book} 作者 [string]:"Robert C. Martin" 类别 [string]:"Computers" Id [string]:"5df2b193405b7e9c1efa2870" 元数据 [BsonDocument]:null 名称 [string]: “清洁码”价格[十进制]: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).Items2 ).Metadata.AsBsonRegularExpression' 抛出了类型为 'System.InvalidCastException' AsBsonSymbol [BsonSymbol]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonSymbol' 抛出了类型为 ' 的异常System.InvalidCastException' AsBsonTimestamp [BsonTimestamp]:'(new System.Collections.Generic.ICollectionDebugView(test).Items 2 ).Metadata.AsBsonTimestamp' 引发了类型为 'System.InvalidCastException' AsBsonUndefined [BsonUndefined]:'(new System .Collections.Generic.ICollectionDebugView(test).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' [More] Name [string]:"A新测试”价格[十进制]:43.15

标签: c#asp.net-core-webapimongodb-.net-driver

解决方案


您应该使用BsonDocumentMongoDB 来处理 C# 中的无类型数据。

private readonly IMongoCollection<BsonDocument> _books;

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

_books.DeleteOne(book => book.Id == id);

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

_books.DeleteOne(book => book["_id"] == id);

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

[BsonId]
[BsonRepresentation(BsonType.ObjectId)]

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

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

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

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

更新以解决额外问题

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

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

此异常似乎表明BsonDocument数据库中有某事是 a,但驱动程序正试图将其分配给一个bool值。我无法重现我的错误。正如您在上面提供的那样,我在数据库中创建了一个文档。 数据库文件

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

您能否提供其余的堆栈跟踪?它可能会为我们提供有关导致问题的字段的更多信息。您也可以尝试BsonExtraElementsMetadata您的 POCO 中删除并创建一个新字段BsonExtraElements

更新 3

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

ABsonDocument是一种惰性类型。在您尝试访问它之前,它不“知道”它包含的内容。这是通过为许多不同的字段提供一个 getter 来处理的,所有字段都由它可以包含的类型命名。你可以在这里看到它们。

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

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


推荐阅读