Reputation: 758
I have the following stripped-down DTO:
[DataContract]
public class ChartDefinitionBase
{
[DataMember]
public string Id { get; private set; }
}
... and the following stripped-down Mongo service definition:
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;
}
}
I then have a client library which has a WCF service reference to MongoChartService
named ChartServiceClient
.
When I create an instance of MongoChartService
directly and inject an instance of ChartDefinitionBase
(fully implemented and no child classes), I can complete a round trip to the database (create, read, delete). If I create an instance of ChartServiceClient
and try to repeat the same steps with the stripped-down DTO, I get a ServiceModel.FaultException
when GetAllCharts
is called, with ExceptionDetail
"An item with the same key has already been added." Here is an example unit test with comments.
[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);
}
}
}
The stack trace is as follows:
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()
Edit: Note that the exception seems to occur with the call to BsonClassMap.RegisterClassMap
. This method seems to be called with both Create
and GetAllCharts()
.
Anybody have any idea what's going on and how to fix this issue?
Upvotes: 3
Views: 5051
Reputation: 11785
If you have several applications using the MongoDB layer as a library, you'd probably prefer the class mappings in a static constructor rather than each application's startup.
One thing to be careful of with that is if you're putting the mappings in a base class that's generic, the static constructor can still be called multiple times (once for each type). The IsClassMapRegistered
check I mentioned above will help most of the time, but it's not thread safe. If you're still getting the exception, look at the stack trace. If there are asynchronous methods in the call stack, you're running into a thread safety issue where both threads are determining the class map is not registered but then one thread beats the other to the chase and the second throws an exception. The best way to handle that is to use a singleton for your class mappings, and wrapping the class mappings in a lock
statement.
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;
}
}
Your usage would look something like:
BsonClassMapper.Instance
.Register<User>(cm => {
cm.Automap();
})
.Register<Order>(cm => {
cm.AutoMap();
});
Upvotes: 4
Reputation: 758
The issue seems to be due to placement of the call to BsonClassMap.RegisterClassMap
in the constructor of MongoChartService
. When using MongoChartService
directly, the constructor is called only once. When using ChartServiceClient
, the MongoChartService
constructor is called once on Create
and once on GetAllCharts
; however, since ChartDefinitionBase
was registered the first time around, the second attempt to register it produces the exception.
The problems resolves when I either use BsonIdAttribute
on Id
or move the call to BsonClassMap.RegisterClassMap
elsewhere, for instance above the call to the client:
[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);
}
}
}
The MongoDb documentation states something to this effect, although I hadn't appreciated that the service constructor would be called multiple times:
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.
Upvotes: 3