为什么 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 时似乎会发生异常。此方法似乎与 CreateGetAllCharts().

一起调用

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

问题似乎是由于在 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();
  });