为什么 WCF/Mongo 抛出异常:已添加具有相同键的项目
Why is WCF/Mongo throwing exception: An item with the same key has already been added
我有以下精简的 DTO:
[DataContract]
public class ChartDefinitionBase
{
[DataMember]
public string Id { get; private set; }
}
... 以及以下精简的 Mongo 服务定义:
public class MongoChartService : IChartService
{
private readonly IMongoCollection<ChartDefinitionBase> _collection;
private const string _connectionStringKey = "MongoChartRepository";
internal MongoChartService()
{
// Exception occurs here.
BsonClassMap.RegisterClassMap<ChartDefinitionBase>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
var connectionString = ConfigurationManager.ConnectionStrings[_connectionStringKey].ConnectionString;
var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
var client = new MongoClient(settings);
var database = client.GetDatabase(ConfigurationManager.ConnectionStrings[_connectionStringKey].ProviderName);
_collection = database.GetCollection<ChartDefinitionBase>("Charts");
}
public void Create(ChartDefinitionBase instance)
{
_collection.InsertOne(instance);
}
public IEnumerable<ChartDefinitionBase> GetAllCharts()
{
var charts = _collection.Find(_ => true).ToList();
return charts;
}
}
然后我有一个客户端库,它有一个对 MongoChartService
的 WCF 服务引用,名为 ChartServiceClient
。
当我直接创建一个MongoChartService
的实例并注入一个ChartDefinitionBase
的实例(完全实现并且没有子类),我可以完成一个到数据库的往返(创建、读取、删除)。如果我创建 ChartServiceClient
的实例并尝试使用精简的 DTO 重复相同的步骤,我会在 GetAllCharts
被调用时得到 ServiceModel.FaultException
,其中 ExceptionDetail
"An item with the same key has already been added." 这是一个带有注释的单元测试示例。
[TestMethod, TestCategory("MongoService")]
public void ChartServiceClient_CRD_ExecutesSuccessfully()
{
SetupHost();
using (var client = new ChartServiceClient())
{
client.Create(_dto); // Create method succeeds. Single entry in dB with Mongo-generated ID.
ChartDefinitionBase dto = null;
while (dto == null)
{
var dtos = client.GetAllCharts(); // Exception occurs here.
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
client.Delete(_dto);
while (dto != null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
}
}
堆栈跟踪如下:
Server stack trace:
at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter)
at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
Exception rethrown at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at QRPad.Spc.DataLayer.Charts.Service.Client.ServiceReference.IChartService.GetAllCharts()
at QRPad.Spc.DataLayer.Charts.Service.Client.ServiceReference.ChartServiceClient.GetAllCharts()
编辑: 请注意,调用 BsonClassMap.RegisterClassMap
时似乎会发生异常。此方法似乎与 Create
和 GetAllCharts()
.
一起调用
有人知道发生了什么以及如何解决这个问题吗?
问题似乎是由于在 MongoChartService
的构造函数中放置了对 BsonClassMap.RegisterClassMap
的调用。直接使用 MongoChartService
时,构造函数只被调用一次。使用 ChartServiceClient
时,MongoChartService
构造函数在 Create
上调用一次,在 GetAllCharts
上调用一次;然而,由于 ChartDefinitionBase
是第一次注册,第二次尝试注册它会产生异常。
当我在 Id
上使用 BsonIdAttribute
或将调用移到其他地方的 BsonClassMap.RegisterClassMap
时,问题解决了,例如在对客户端的调用之上:
[TestMethod, TestCategory("MongoService")]
public void ChartServiceClient_CRD_ExecutesSuccessfully()
{
SetupHost();
BsonClassMap.RegisterClassMap<ChartDefinitionBase>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
using (var client = new ChartServiceClient())
{
client.Create(_dto);
ChartDefinitionBase dto = null;
while (dto == null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
client.Delete(_dto);
while (dto != null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
}
}
MongoDb documentation 说明了这方面的内容,尽管我没有意识到服务构造函数会被多次调用:
It is very important that the registration of class maps occur prior
to them being needed. The best place to register them is at app
startup prior to initializing a connection with MongoDB.
如果您有多个应用程序使用 MongoDB 层作为库,您可能更喜欢静态构造函数中的 class 映射,而不是每个应用程序的启动。
需要注意的一件事是,如果您将映射放在通用的基础 class 中,静态构造函数仍然可以被调用多次(每种类型一次)。我上面提到的 IsClassMapRegistered
检查在大多数情况下都会有所帮助,但它不是线程安全的。如果您仍然遇到异常,请查看堆栈跟踪。如果调用堆栈中有异步方法,那么您 运行 就会陷入线程安全问题,其中两个线程都确定 class 映射未注册,但随后一个线程先于另一个线程追逐,然后第二个抛出异常。处理该问题的最佳方法是为 class 映射使用单例,并将 class 映射包装在 lock
语句中。
public sealed class BsonClassMapper{
private static BsonClassMapper instance = null;
private static readonly object _lock = new object();
public static BsonClassMapper Instance {
get {
if(instance == null){
instance = new BsonClassMapper();
}
return instance;
}
}
public BsonClassMapper Register<T>(Action<BsonClassMap<T>> classMapInitializer){
lock(_lock){
if(!BsonClassMap.IsClassMapRegistered(typeof(T))){
BsonClassMap.RegisterClassMap<T>(classMapInitializer);
}
}
return this;
}
}
您的用法类似于:
BsonClassMapper.Instance
.Register<User>(cm => {
cm.Automap();
})
.Register<Order>(cm => {
cm.AutoMap();
});
我有以下精简的 DTO:
[DataContract]
public class ChartDefinitionBase
{
[DataMember]
public string Id { get; private set; }
}
... 以及以下精简的 Mongo 服务定义:
public class MongoChartService : IChartService
{
private readonly IMongoCollection<ChartDefinitionBase> _collection;
private const string _connectionStringKey = "MongoChartRepository";
internal MongoChartService()
{
// Exception occurs here.
BsonClassMap.RegisterClassMap<ChartDefinitionBase>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
var connectionString = ConfigurationManager.ConnectionStrings[_connectionStringKey].ConnectionString;
var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
var client = new MongoClient(settings);
var database = client.GetDatabase(ConfigurationManager.ConnectionStrings[_connectionStringKey].ProviderName);
_collection = database.GetCollection<ChartDefinitionBase>("Charts");
}
public void Create(ChartDefinitionBase instance)
{
_collection.InsertOne(instance);
}
public IEnumerable<ChartDefinitionBase> GetAllCharts()
{
var charts = _collection.Find(_ => true).ToList();
return charts;
}
}
然后我有一个客户端库,它有一个对 MongoChartService
的 WCF 服务引用,名为 ChartServiceClient
。
当我直接创建一个MongoChartService
的实例并注入一个ChartDefinitionBase
的实例(完全实现并且没有子类),我可以完成一个到数据库的往返(创建、读取、删除)。如果我创建 ChartServiceClient
的实例并尝试使用精简的 DTO 重复相同的步骤,我会在 GetAllCharts
被调用时得到 ServiceModel.FaultException
,其中 ExceptionDetail
"An item with the same key has already been added." 这是一个带有注释的单元测试示例。
[TestMethod, TestCategory("MongoService")]
public void ChartServiceClient_CRD_ExecutesSuccessfully()
{
SetupHost();
using (var client = new ChartServiceClient())
{
client.Create(_dto); // Create method succeeds. Single entry in dB with Mongo-generated ID.
ChartDefinitionBase dto = null;
while (dto == null)
{
var dtos = client.GetAllCharts(); // Exception occurs here.
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
client.Delete(_dto);
while (dto != null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
}
}
堆栈跟踪如下:
Server stack trace:
at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter)
at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
Exception rethrown at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at QRPad.Spc.DataLayer.Charts.Service.Client.ServiceReference.IChartService.GetAllCharts()
at QRPad.Spc.DataLayer.Charts.Service.Client.ServiceReference.ChartServiceClient.GetAllCharts()
编辑: 请注意,调用 BsonClassMap.RegisterClassMap
时似乎会发生异常。此方法似乎与 Create
和 GetAllCharts()
.
有人知道发生了什么以及如何解决这个问题吗?
问题似乎是由于在 MongoChartService
的构造函数中放置了对 BsonClassMap.RegisterClassMap
的调用。直接使用 MongoChartService
时,构造函数只被调用一次。使用 ChartServiceClient
时,MongoChartService
构造函数在 Create
上调用一次,在 GetAllCharts
上调用一次;然而,由于 ChartDefinitionBase
是第一次注册,第二次尝试注册它会产生异常。
当我在 Id
上使用 BsonIdAttribute
或将调用移到其他地方的 BsonClassMap.RegisterClassMap
时,问题解决了,例如在对客户端的调用之上:
[TestMethod, TestCategory("MongoService")]
public void ChartServiceClient_CRD_ExecutesSuccessfully()
{
SetupHost();
BsonClassMap.RegisterClassMap<ChartDefinitionBase>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
using (var client = new ChartServiceClient())
{
client.Create(_dto);
ChartDefinitionBase dto = null;
while (dto == null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
client.Delete(_dto);
while (dto != null)
{
var dtos = client.GetAllCharts();
dto = dtos.SingleOrDefault(d => d.Id == _dto.Id);
}
}
}
MongoDb documentation 说明了这方面的内容,尽管我没有意识到服务构造函数会被多次调用:
It is very important that the registration of class maps occur prior to them being needed. The best place to register them is at app startup prior to initializing a connection with MongoDB.
如果您有多个应用程序使用 MongoDB 层作为库,您可能更喜欢静态构造函数中的 class 映射,而不是每个应用程序的启动。
需要注意的一件事是,如果您将映射放在通用的基础 class 中,静态构造函数仍然可以被调用多次(每种类型一次)。我上面提到的 IsClassMapRegistered
检查在大多数情况下都会有所帮助,但它不是线程安全的。如果您仍然遇到异常,请查看堆栈跟踪。如果调用堆栈中有异步方法,那么您 运行 就会陷入线程安全问题,其中两个线程都确定 class 映射未注册,但随后一个线程先于另一个线程追逐,然后第二个抛出异常。处理该问题的最佳方法是为 class 映射使用单例,并将 class 映射包装在 lock
语句中。
public sealed class BsonClassMapper{
private static BsonClassMapper instance = null;
private static readonly object _lock = new object();
public static BsonClassMapper Instance {
get {
if(instance == null){
instance = new BsonClassMapper();
}
return instance;
}
}
public BsonClassMapper Register<T>(Action<BsonClassMap<T>> classMapInitializer){
lock(_lock){
if(!BsonClassMap.IsClassMapRegistered(typeof(T))){
BsonClassMap.RegisterClassMap<T>(classMapInitializer);
}
}
return this;
}
}
您的用法类似于:
BsonClassMapper.Instance
.Register<User>(cm => {
cm.Automap();
})
.Register<Order>(cm => {
cm.AutoMap();
});