添加聚合管道阶段,更改为 MongoDB C# 驱动程序中的另一个模型或类型

问题描述 投票:0回答:1

如何添加聚合管道阶段来更改正在运行聚合的集合模型输出?

X/Y 问题:如何聚合查找在另一个文档中引用为 ObjectId 的文档,并在单个查询中返回嵌套文档,而不在集合模型中添加冗余字段?

简单模型:

public class Foo
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;
}

public class Bar
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonElement("foo")]
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

因为我想将

Foo
存储在
Bar
中作为参考,但也能够获取填充的对象:

public class BarDb
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    public string? Name { get; set; } = null!;

    [BsonRepresentation(BsonType.ObjectId)]
    [BsonElement("foo")]
    public string? FooId { get; set; } = null!;
}

public class BarDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonRepresentation(BsonType.ObjectId)]
    [BsonElement("foo")]
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

一个简单流畅的查找聚合

private IAggregateFluent<BarDTO> GetDefaultPipeline()
{
    return barDbCollection
        .Aggregate()
        .Lookup<BarDb, Foo, BarDTO>(
            fooCollection,
            localField => localField.FooId,
            foreignField => foreignField.Id,
            asField => asField.Foo
        )
        .Unwind(
            field => field.Foo,
            new AggregateUnwindOptions<BarDTO>
            {
                PreserveNullAndEmptyArrays = true,
            }
        );
}

MongoDB.Driver.Linq.ExpressionNotSupportedException: Expression not supported: asField.Foo
,很公平。

我尝试用普通的旧式写聚合

BsonDocument
:

public class BarDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonElement("foo_object")] // let it accept an additional field from lookup
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

private IAggregateFluent<BarDTO> GetDefaultPipeline()
{
    var lookupStage = new BsonDocument(
        "$lookup",
        new BsonDocument
        {
            { "from", "foo" },
            { "localField", "foo" },
            { "foreignField", "_id" },
            { "as", "foo_object" },
        }
    );
    var unwindStage = new BsonDocument(
        "$unwind",
        new BsonDocument
        {
            { "path", "$foo_object" },
            { "preserveNullAndEmptyArrays", true },
        }
    );
    return barDbCollection
        .Aggregate()
        .AppendStage<BarDTO>(lookupStage) // attempt to change the output model
        .AppendStage<BarDTO>(unwindStage);
}

Element 'foo_object' does not match any field or property of class BarDb
,所以它仍然停留在
barDbCollection
的模型上。

如果我向 BarDb 模型添加一个附加字段

foo_object
,它将起作用:

public class BarDb
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonRepresentation(BsonType.ObjectId)]
    [BsonElement("foo")]
    [JsonIgnore]
    public string? FooId { get; set; } = null!;

    [BsonElement("foo_object")] // let it accept an additional field from lookup
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

private IAggregateFluent<BarDTO> GetDefaultPipeline()
{
    // ...

    return barDbCollection
        .Aggregate()
        .AppendStage<BarDb>(lookupStage) // don't have to change the model anymore
        .AppendStage<BarDb>(unwindStage);
}

这可行,但是当我插入带有

barDbCollection
的文档时,
Bar
文档将始终有一个冗余的
foo_object
字段,其值为
null
:

public async Task CreateFooInBar(string fooName, string barName)
{
    var foo = new Foo { Name = fooName };
    await fooCollection.InsertOneAsync(foo);
    await barDbCollection.InsertOneAsync(
        new BarDb { Name = barName, FooId = foo.Id }
    );
}

结果:

Bar
文档将始终有一个冗余的
foo_object
字段,其值为
null
:

{
    "_id": {
        "$oid": "673def40112d1449afea9ddc"
    },
    "name": "I am Foo!"
}

{
    "_id": {
        "$oid": "673def40112d1449afea9ddd"
    },
    "name": "I am Bar!",
    "foo": {
        "$oid": "673def3f112d1449afea9ddc"
    },
    "foo_object": null
}

我知道还有其他方法:

  • 首先获取Db模型,然后进行更多查询来填充DTO模型:这是我试图避免的,所以我使用查找聚合来避免多次查询。

  • 将查询结果读取为BsonDocument:这需要手动分配字段,这也是我试图避免的。

  • 使用

    ReadToDTO
    模型及其自己的集合,与 CRUD Db 模型分开:

public class BarReadToDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonRepresentation(BsonType.ObjectId)]
    [BsonElement("foo")]
    [JsonIgnore] // do not include this in the deserialized json string
    public string? FooId { get; set; } = null!;

    [BsonElement("foo_object")] // let it accept an additional field from lookup
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

但是有一些复杂性:这是一个学校项目,我需要让所有存储库继承自通用 CRUD 存储库,这就是它的完成方式:

public class BaseDbModel
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;
}

public interface ICrudRepository<T> : IMongoDbRepository<T>
    where T : BaseDbModel
{
    Task<IEnumerable<T>> FindAllAsync();
    Task<T?> FindByIdAsync(string id);
    Task InsertAsync(T entity);
    Task ReplaceAsync(string id, T entity);
    Task DeleteAsync(string id);
}

通过另一个

Read
模型,可能会使通用 CRUD 存储库变得过于复杂。

已安装的软件包:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MongoDB.Driver" Version="2.29.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>
c# mongodb linq aggregation-framework mongodb-.net-driver
1个回答
0
投票

我测试了您的代码并从 DTO 的第一个版本开始。为了解决这个问题,您需要采取两个步骤:

第一个非常快:从

BsonRepresenation
中的
Foo
属性中删除
BarDb
属性。这会删除
UnsupportedException

此外,您需要创建一个临时 DTO,它采用一组

Foo
对象,而不是单个对象(尽管通过 id 查询将检索一个对象)。我在以下示例中将临时 DTO 类称为
TempBarDTO


public class BarDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonElement("foo")]
    [JsonPropertyName("foo")]
    public Foo? Foo { get; set; } = null!;
}

public class TempBarDTO
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    [JsonPropertyName("id")]
    public string? Id { get; set; } = null!;

    [BsonElement("name")]
    [JsonPropertyName("name")]
    public string? Name { get; set; } = null!;

    [BsonElement("foo")]
    [JsonPropertyName("foo")]
    public IEnumerable<Foo>? Foo { get; set; } = null!;
}

此后,您可以运行以下代码来检索文档:

var result = barDbCollection.Aggregate()
    .Lookup<BarDb, Foo, TempBarDTO>(fooCollection, x => x.FooId, x => x.Id, x => x.Foo)
    .Project(x => new BarDTO()
    {
        Id = x.Id, 
        Name = x.Name,
        Foo = x.Foo!.FirstOrDefault(),
    })
    .ToList();

上面的代码应该给你一个如何解决这个问题的概述。还有其他方法,例如没有临时 DTO 类并添加计算属性以从

Foo
中的数组检索第一个
BarDTO
。如果您有很多属性必须包含在
Project
阶段或者您希望这些属性经常更改,那么这会更容易。

© www.soinside.com 2019 - 2024. All rights reserved.