如何添加聚合管道阶段来更改正在运行聚合的集合模型输出?
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>
我测试了您的代码并从 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
阶段或者您希望这些属性经常更改,那么这会更容易。