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

标签 c# asp.net-core-webapi mongodb-.net-driver

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

本教程和我发现的其他教程一样,都使用定义的数据模型(在上面的教程中是 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 操作的能力。

作为尝试和错误尝试,我用“对象”和“动态”替换了“书籍”模型,但我遇到了关于转换类型和未知属性的各种错误:

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' 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'.

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

更新:基于@pete-garafano suggestion ,我决定继续使用 POCO 模型。

我找到了一个 article in MongoDB's 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: Unable to cast object of type 'MongoDB.Bson.BsonElement' to type 'MongoDB.Bson.BsonDocument'.

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

Mongo Compass Document Listing

System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonDocument' to type 'MongoDB.Bson.BsonBoolean'.

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

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

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)

更新 #3:添加了 Book 服务和 Controller 代码文件、数据库 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} 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

最佳答案

您应该使用 BsonDocument 在 C# 中使用 MongoDB 处理非类型化数据。

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# 中的字符串应该被视为 ObjectId 由 MongoDB 编写。

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

我写了一个post前一段时间可能对你有帮助。它涵盖了许多与 Microsoft 帖子相同的 Material ,但可能会为您提供更多见解。

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

更新以解决额外问题

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

System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonDocument' to type 'MongoDB.Bson.BsonBoolean'.

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

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

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

更新3

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

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

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

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

关于c# - 使用通用 JSON 对象的 .NET Core Web API 和 MongoDB 驱动程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59165779/

相关文章:

c# - 快速多线程问题

c# - 为 WebAPI 中的 Enum 描述实现一个 TypeConverter

asp.net-core - 使用 ASP.NET Core Web API 将依赖项注入(inject)验证属性

c# - 如何在 C# 中全局定义常量(如 DEBUG)

c# - WPF Storyboard : DoubleAnimation Works On Opacity But Not TranslateTransform?

azure - 限制 Azure AD 用户访问 Web api Controller

c# - 使用 C# 查询 MongoDB 嵌套数组文档

c# - 当主服务器出现故障时,有没有办法自动使 MongoDB C# 驱动程序不抛出 EndOfStreamException ?

c# - 使用 C# 驱动程序是复制和复制 MongoDB 集合的更好方法吗?

c# - EF Core SetQueryFilter 在 OnModelCreating 中将 IsActive 反向为 IsDeleted