为啥 WCF/Mongo 抛出异常:已添加具有相同键的项目

Posted

技术标签:

【中文标题】为啥 WCF/Mongo 抛出异常:已添加具有相同键的项目【英文标题】:Why is WCF/Mongo throwing exception: An item with the same key has already been added为什么 WCF/Mongo 抛出异常:已添加具有相同键的项目 【发布时间】:2016-09-01 20:46:39 【问题描述】:

我有以下精简的 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.FaultExceptionExceptionDetail“具有相同键的项目已经添加。”这是一个使用 cmets 的示例单元测试。

    [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 时似乎会发生异常。此方法似乎同时使用CreateGetAllCharts() 调用。

有人知道发生了什么以及如何解决此问题吗?

【问题讨论】:

【参考方案1】:

问题似乎是由于在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 说明了这种情况,尽管我没有意识到服务构造函数会被多次调用:

类映射的注册在先发生是非常重要的 他们被需要。注册它们的最佳位置是在应用程序 在初始化与 MongoDB 的连接之前启动。

【讨论】:

由于它是一个全局配置,如果执行两次就会中断,您可以先检查它是否尚未在if(!BsonClassMap.IsClassMapRegistered(typeof(ChartDefinitionBase)))注册【参考方案2】:

如果您有多个应用程序使用 MongoDB 层作为库,您可能更喜欢静态构造函数中的类映射,而不是每个应用程序的启动。

需要注意的一点是,如果您将映射放在通用的基类中,静态构造函数仍然可以被多次调用(每种类型一次)。我上面提到的IsClassMapRegistered 检查在大多数情况下都会有所帮助,但它不是线程安全的。如果您仍然收到异常,请查看堆栈跟踪。如果调用堆栈中有异步方法,您将遇到线程安全问题,其中两个线程都确定类映射未注册,但随后一个线程击败另一个线程,第二个线程引发异常。处理这个问题的最佳方法是为您的类映射使用单例,并将类映射包装在 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();
  );

【讨论】:

以上是关于为啥 WCF/Mongo 抛出异常:已添加具有相同键的项目的主要内容,如果未能解决你的问题,请参考以下文章

为啥不抛出异常的代码允许捕获已检查的异常?

对话框片段已添加异常未抛出

为啥要尝试在已检查的异常上抛出未检查的异常? [复制]

为啥在这种情况下允许抛出已检查的异常类型?

ZeroMQ 套接字 Recv() 抛出“上下文已终止”异常 - 为啥以及如何恢复?

已添加具有相同键的实体框架核心 3.1.6 项