I have made an application that uses SignalR to occasionally fetch new data and show it to the users in real time. The problem is that my services are static (in my case I have named them Utilities) and inconsistently I receive errors which lead me to thinking that I have a concurrency problem with the context. Logically my services are best to be static. Here's my code, can you suggest me a better design pattern or something so I can still have my services static, but don't run into the errors from concurrent EF usage.
This is where I give each of my static services a DbContext to work with:
public static class Common
{
[ThreadStatic]
private static CryptoStatisticsDbContext _dbcontext;
internal static CryptoStatisticsDbContext DbContext => _dbcontext ?? (_dbcontext = new CryptoStatisticsDbContext());
}
Here are some of my services
TickerUtility
public static class TickerUtility
{
public delegate void UpdateTickerDelegate(int currencyId, decimal bid, decimal ask);
public static event UpdateTickerDelegate TickerUpdatedEvent;
public static List<Ticker> LastTickers { get; private set; } = new List<Ticker>();
public static IList<Ticker> GetAllTickers(Action<Ticker> actionToDoWithEachTicker)
{
var tickers = new ConcurrentBag<Ticker>();
var allCurrencies = Common.DbContext.Currencies.Include(c => c.Market).ToList();
Parallel.ForEach(allCurrencies, currency =>
//foreach (var currency in allCurrencies)
{
var ticker = GetTicker(currency);
tickers.Add(ticker);
actionToDoWithEachTicker?.Invoke(ticker);
});
return tickers.ToList();
}
}
MarketUtility
public static class MarketUtility
{
public static List<Market> Get()
{
return Common.DbContext.Markets.Include(m => m.Currencies).ToList();
}
}
SignalRUtility
public static class SignalRUtility
{
private static readonly IHubContext PublicMarketsHub = GlobalHost.ConnectionManager.GetHubContext<PublicMarketsHub>();
private static readonly IHubContext AdminMarketsHub = GlobalHost.ConnectionManager.GetHubContext<AdminMarketsHub>();
public static void SendNewTickersToClients(object state)
{
TickerUtility.GetAllTickers(ticker =>
{
if (ticker.IsVisibleForPublic)
PublicMarketsHub.Clients.Group(ticker.CurrencyId.ToString()).addNewTicker(ticker);
AdminMarketsHub.Clients.Group(ticker.CurrencyId.ToString()).addNewTicker(ticker);
});
}
public static void UpdateBalances(object state)
{
BalancesUtility.GetAvailableBalances(balance => AdminMarketsHub.Clients.All.showBalanceForMarket(balance),
(market) => AdminMarketsHub.Clients.All.showError($"Error getting {market} balances")
);
}
}
The BackgroundTimer which calls the static functions
public class BackgroundServerTimer : IRegisteredObject
{
private Timer ratesTimer;
private Timer balancesTimer;
private readonly TimeSpan ratesTimerInterval = new TimeSpan(0, 0, 0, 20);
private readonly TimeSpan balancesTimerInterval = new TimeSpan(0, 0, 0, 60);
public BackgroundServerTimer()
{
StartTimers();
}
private void StartTimers()
{
var delayStartby = new TimeSpan(0,0,0, 1);
ArbitrageUtility.AttachListenerToTickerChanges();
ratesTimer = new Timer(SignalRUtility.SendNewTickersToClients, null, delayStartby, ratesTimerInterval);
balancesTimer = new Timer(SignalRUtility.UpdateBalances, null, delayStartby, balancesTimerInterval);
}
public void Stop(bool immediate)
{
ratesTimer.Dispose();
balancesTimer.Dispose();
HostingEnvironment.UnregisterObject(this);
}
}
To elaborate on my problem see my HomeController:
public class HomeController : Controller
{
public ActionResult Index()
{
var markets = MarketUtility.Get().Select(m =>
{
m.Currencies = m.Currencies.OrderBy(c => c.Id).ToList();
return m;
});
var isUserAdmin = User.IsInRole("admin");
if (!isUserAdmin)
{
markets = markets.Select(m =>
{
m.Currencies = m.Currencies.Where(c => c.IsVisibleForPublic).ToList();
return m;
}).ToList();
markets = markets.Where(m => m.Currencies.Any()).ToList();
}
ViewBag.Markets = markets;
return isUserAdmin ? View("AdminIndex") : View();
}
}
One of my markets has 4 currencies. 2 public and 2 private. Sometimes when I view the app as a guest it shows me the 2 public, but after that I login with the admin but MarketUtility.Get() again returns me only the 2 public currencies (without me implying any .Where() clause). After some refreshes it gives me all 4 currencies. The same happens on edit and delete operations. I edit some entity (for example a currency) and the first couple of refreshes show different data. Sometimes the old data, sometimes the new data until (I suppose) EF clears it's cache and then I start to get the fresh data. I have disabled LazyLoading and ProxyCreation, now the problems occurs more rarely, but I know this is not the solution.
Parallel.ForEach(allCurrencies, currency =>
//foreach (var currency in allCurrencies)
{
var ticker = GetTicker(currency);
tickers.Add(ticker);
actionToDoWithEachTicker?.Invoke(ticker);
});
This is the line that throws exceptions like "Collection was Modified", "“The underlying provider failed on Open.” (this one I fixed using MARS, but again this is just a workaround). I don't get how can it throw collection was modified error when I use .ToList()
var allCurrencies = Common.DbContext.Currencies.Include(c => c.Market).ToList();
I want to make things the right way, to dispose the context when it's not needed, to consume less memory and everything, but still have my services static because it makes it easier for me (and I don't see a point in making them non static provided that all my methods are static
Related
I discovered a weird behavior where I absolutely don't know where it comes from or how to fix it.
The issue arises with the blazor-state management (which is based on the mediator pattern) - library can be found here: https://timewarpengineering.github.io/blazor-state/.
Lets assume we have the following base class for an enumeration:
public abstract class Simple<TSimple> where TSimple: Simple<TSimple>
{
private readonly string _key;
protected Simple(string key)
{
_key = key;
}
public virtual string Key => _key;
public static TSimple Create(string key)
{
var obj = All.SingleOrDefault(e => e.Key == key);
return obj;
}
public static IReadOnlyCollection<TSimple> All => GetAll();
private static IReadOnlyCollection<TSimple> GetAll()
{
var enumerationType = typeof(TSimple);
return enumerationType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(info => enumerationType.IsAssignableFrom(info.FieldType))
.Select(info => info.GetValue(null))
.Cast<TSimple>()
.ToArray();
}
}
And the following enumeration implementation:
public class SimpleImpl : Simple<SimpleImpl>
{
public static readonly SimpleImpl One = new SimpleImpl("Important");
public static readonly SimpleImpl Two = new SimpleImpl("Urgent");
public static readonly SimpleImpl Three = new SimpleImpl("ImportantAndUrgent");
public static readonly SimpleImpl Four = new SimpleImpl("None");
private SimpleImpl(string key) : base(key)
{
}
}
So far so good.
I use this enumeration in a blazor app, where the data is retrieved via gRPC-Web from the backend, is transformed and added to the state.
So the code section of the Index.cshtml looks something like this:
#code
{
private AppState AppState => GetState<AppState>();
protected override async Task OnInitializedAsync()
{
foreach (var simple in new[] {"Important", "Urgent", "ImportantAndUrgent", "None"})
{
await Mediator.Send(new AppState.AddAction(simple));
}
}
This gets handled by the Handler:
public partial class AppState
{
public class AppHandler : ActionHandler<AddAction>
{
private AppState AppState => Store.GetState<AppState>();
public AppHandler(IStore store) : base(store)
{
}
public override async Task<Unit> Handle(AddAction aAction, CancellationToken aCancellationToken)
{
var simple = SimpleImpl.Create(aAction.Simple);
Console.WriteLine(simple == null); // First call false, afterwards true
AppState.Simples.Add(simple); // If I don't add the object to the state, Simple.Create always returns an object
return await Unit.Task;
}
}
}
And here is the problem. On the first try everything works, but if the functions gets called a second time (so my gRPC-Client returns multiple items) simple will always be null. If I remove the AppState.Simples.Add(simple) then it works again.
If I add the following code: Console.WriteLine(string.Join(",", SimpleImpl.All.Select(s => s.Key)); on the first run it prints all the possible values:
Important,Urgent,ImportantAndUrgent,None
On the second run, this:
,Urgent,,
Urgent was in the Dto in the first run. So it seems something to do with how the reference in the List is kept alive (which should not interfer with how the reflection part in Simple works).
Furthermore: in the GetAll() function of Simple everything works fine until the Select(info => .GetValue(null)) The FieldInfo-Property itself holds all 4 options. After GetValue and the cast there is only the last choosen one "alive".
The State-Entity looks like the following:
public partial class AppState : State<AppState>
{
public IList<SimpleImpl> Simples { get; private set; }
public override void Initialize()
{
Simples = new List<SimpleImpl>();
}
}
And the Action of this sample:
public partial class AppState
{
public class AddAction : IAction
{
public AddAction(string simple)
{
Simple = simple;
}
public string Simple { get; }
}
}
This code is running under .NET Core 3.1.
If anybody has a tip where the problem lays I would be very thankful.
Thanks to #steven-t-cramer how helped me on finding the issue.
Basically it all boils down to the Mediator.Send and State-Handling.
In the Blazor-State library a clone is created when one dispatches and handles an action (so you as a developer don't have to take care of that). But exactly this cloning messed up big time here because of the static nature of Simple(basically an enumeration class).
To get around that, the state can implement ICloneable and do this stuff on its own.
A very naive way to do would be that:
public partial class AppState : State<AppState>, ICloneable
{
private List<SimpleImpl> _simples = new List<SimpleImpl>();
public IReadOnlyList<SimpleImpl> Simples => _simples.AsReadOnly();
public override void Initialize()
{
_simples = new List<SimpleImpl>();
}
public object Clone()
{
var state = new AppState { _simples = _simples};
return state;
}
}
I want trained machine learning model to be automatically reloaded after a new version is available, so I set watchForChanges to true:
services.AddPredictionEnginePool<SentimentData, SentimentPrediction>()
.FromFile(
modelName: Constants.ModelName,
filePath: Constants.ModelFileName,
watchForChanges: true);
It seems that it works only if prediction is not made before model is retrained.
This is an exception I see in events:
Should I do something else to get desired result?
You can see sample project demonstrating this issue: https://github.com/alexandermujirishvili/DotnetMLWatchForChanges/tree/master
It's not possible
But you can create a factory class for look this
and with object pool design pattern
public class PredictionEngineObjPool<T>
{
private readonly ConcurrentBag<T> _objects;
private readonly Func<T> _objectGenerator;
public PredictionEngineObjPool(Func<T> objectGenerator)
{
_objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
_objects = new ConcurrentBag<T>();
}
public T Get()
{
var result = _objects.TryTake(out T item) ? item : _objectGenerator();
return result;
}
public void Return(T item)
{
_objects.Add(item);
}
}
then create a manager
public class PredictionManager
{
public Tdest Predict<Tsrc, Tdest>(string modelName,Tsrc example) where Tsrc : class where Tdest : class, new()
{
var mlContext = new MLContext();
//create your engine
}
}
use in your program
var pool = new PredictionEngineObjPool<PredictionManager>(() => new PredictionManager());
var _predictionManager = pool.Get();
var prediction= _predictionManager.Predict<ModelInput, ModelOutput>("name", input);
Question
How do I define an incoming Type T constraint that will allow me to call a static method on the class (of type T) to get the intended IndexModel object for passing to Mongo?
Background
I'm currently trying to write a Mongo Provider class that will allow me to ensure my particular database and collection are present before doing any operations with them, since there is a potential that the container or server it resides in could be destroyed and recreated at any time, and I'd prefer to have a safe way in code to ensure that the external dependency is there (instance is beyond my control, so I have to trust that something is there).
One of the things I'm trying to do, since I've managed to do what I stated above for Database and Collection instantiation, is to also generate indexes. My idea was to have a static method on the classes that would return their specific definition of an index model. This way, each class would be responsible for their own Mongo indexes, rather than some convoluted switch-case statement in my Provider based on the incoming type of T.
My first idea was to have an interface that shared this method, but Interfaces don't allow you to declare a static method. Similarly, I tried an Abstract Base-class and found that the static implementation would call the base class that defined the method, rather than any overrides in an inheritor.
Sample Code
public class MyClass
{
public DateTime DateValue { get; set; }
public int GroupId { get; set; }
public string DataType { get; set; }
public static IEnumerable<CreateIndexModel<MyClass>> GetIndexModel(IndexKeysDefinitionBuilder<MyClass> builder)
{
yield return new CreateIndexModel<MyClass>(
builder.Combine(
builder.Descending(entry => entry.DateValue),
builder.Ascending(entry => entry.GroupId),
builder.Ascending(entry => entry.DataType)
)
);
}
}
Edit
I guess I should probably include a shell of my Mongo Provider class. See below:
Edit #2 due to questions about how this hasn't solved my problem, I'm updating the MongoProvider to have the problematic code. Note: Once this method is included, the class will no longer compile, since it isn't possible given what I've done thus far.
public class MongoProvider
{
private readonly IMongoClient _client;
private MongoPrivder(ILookup<string, string> lookup, IMongoClient client)
{
_client = client;
foreach(var database in lookup)
foreach(var collection in database)
Initialize(database.Key, collection);
}
public MongoProvider(IConfiguration config) :this(config.GetMongoObjects(), config.GetMongoClient())
{}
public MongoProvider(IConfiguration config, IMongoClient client) : this(config.GetMongoObjects(), client)
{}
private void Initialize(string database, string collection)
{
var db = _client.GetDatabase(database);
if (!db.ListCollectionNames().ToList().Any(name => name.Equals(collection)))
db.CreateCollection(collection);
}
// The Problem
private void InitializeIndex<T>(string database, string collection)
{
IEnumerable<CreateIndexModel<T>> models;
switch (T)
{
case MyClass:
model = MyClass.GetIndexModel();
break;
default:
break;
}
await _client.GetDatabase(database)
.GetCollection<T>(collection)
.Indexes
.CreateManyAsync(models);
}
}
Edit #3
As a stop-gap, I've gone ahead and done something terrible (not sure if it's going to work yet), and I'll supply the example so you can know my best solution thus far.
public static class Extensions
{
#region Object Methods
public static T TryCallMethod<T>(this object obj, string methodName, params object[] args) where T : class
{
var method = obj.GetType().GetMethod(methodName);
if (method != null)
{
return method.Invoke(obj, args) as T;
}
return default;
}
#endregion
}
This allows me to do the following (inside of MongoProvider)
private async void InitializeIndex<T>(string database, string collection) where T : new()
{
var models = new T().TryCallMethod<IEnumerable<CreateIndexModel<T>>>("GetIndexModel");
await _client.GetDatabase(database)
.GetCollection<T>(collection)
.Indexes
.CreateManyAsync(models);
}
Since it doesn't look like I'm going to get an answer to this, I figured I would provide my solution for future searches of this question. Basically, I added an extension method to the base object class, and used reflection to determine if the method I was looking for was there. From there, I returned a value of true or false, depending on if the method was found, and output the return value to a parameter, in the traditional TryGet pattern.
Note to Future Readers
I do not recommend this approach. This is just how I solved my problem for accessing a method on a type of T. Ideally, an instance method would be implemented, and a signature defined in a common Interface, but that wasn't going to work for my use case.
My Answer
public static class Extensions
{
#region Object Methods
public static bool TryCallMethod<T>(this object obj, string methodName, out T result, params object[] args) where T : class
{
result = null;
var method = obj.GetType().GetMethod(methodName);
if (method == null)
return false;
result = method.Invoke(obj, args) as T;
return true;
}
#endregion
}
My data class looks like this (obfuscated from actual usage)
[BsonDiscriminator("data")]
public class DataClass
{
#region Private Fields
private const string MongoCollectionName = "Data";
#endregion
#region Public Properties
public string CollectionName => MongoCollectionName;
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("date_value")]
public DateTime DateValue { get; set; }
[BsonElement("group_id")]
public int GroupId { get; set; }
[BsonElement("data_type")]
public string DataType { get; set; }
[BsonElement("summary_count")]
public long SummaryCount { get; set; }
[BsonElement("flagged_count")]
public long FlaggedCount { get; set; }
[BsonElement("error_count")]
public long ErrorCount { get; set; }
#endregion
#region Constructor
public DataClass()
{
}
public DataClass(int groupId, string dataType = null, long summaryCount = 0, long flaggedCount = 0, long errorCount = 0)
{
Id = ObjectId.GenerateNewId();
DateValue = DateTime.UtcNow;
GroupId = groupId;
DocCount = summaryCount;
DataType = dataType ?? "default_name";
FlaggedCount = flaggedCount;
ErrorCount = errorCount;
}
#endregion
#region Public Methods
public static IEnumerable<CreateIndexModel<AuditEntry>> GetIndexModel(IndexKeysDefinitionBuilder<AuditEntry> builder)
{
yield return new CreateIndexModel<AuditEntry>(
builder.Combine(
builder.Descending(entry => entry.DateValue),
builder.Ascending(entry => entry.GroupId),
builder.Ascending(entry => entry.DataType)
)
);
}
#endregion
}
I would then call the method in the following fashion, inside my MongoProvider class. The ellipses are present to identify that more code exists within the class.
public class MongoProvider : IMongoProvider
{
#region Private Fields
private readonly IMongoClient _client;
#endregion
#region Constructor
...
#endregion
#region Private Methods
private void Initialize(string database, string collection)
{
var db = _client.GetDatabase(database);
if (!db.ListCollectionNames().ToList().Any(name => name.Equals(collection)))
db.CreateCollection(collection);
}
private async Task InitializeIndex<T>(string database, string collection) where T : new()
{
if(new T().TryCallMethod<IEnumerable<CreateIndexModel<T>>>("GetIndexModel", out var models, new IndexKeysDefinitionBuilder<T>()))
await _client.GetDatabase(database)
.GetCollection<T>(collection)
.Indexes
.CreateManyAsync(models);
}
private static void ValidateOptions<T>(ref FindOptions<T, T> options)
{
if(options != null)
return;
options = new FindOptions<T, T>
{
AllowPartialResults = null,
BatchSize = null,
Collation = null,
Comment = "AspNetWebService",
CursorType = CursorType.NonTailable,
MaxAwaitTime = TimeSpan.FromSeconds(10),
MaxTime = TimeSpan.FromSeconds(10),
Modifiers = null,
NoCursorTimeout = false,
OplogReplay = null
};
}
private static FilterDefinition<T> GetFilterDefinition<T>(Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] builders)
{
if(builders.Length == 0)
builders = new Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] {b => b.Empty};
return new FilterDefinitionBuilder<T>()
.And(builders
.Select(b => b(new FilterDefinitionBuilder<T>()))
);
}
#endregion
#region Public Methods
public async Task<IReadOnlyCollection<T>> SelectManyAsync<T>(string database, string collection, FindOptions<T, T> options = null, params Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] builders) where T : new()
{
ValidateOptions(ref options);
await InitializeIndex<T>(database, collection);
var filter = GetFilterDefinition(builders);
var find = await _client.GetDatabase(database)
.GetCollection<T>(collection)
.FindAsync(filter, options);
return await find.ToListAsync();
}
...
#endregion
}
In my asp.net core 2 application, I have a static class which handles some interactions with the session. For example :
public static class BookManager
{
private const string Key = "Books";
public static HttpContextAccessor ContextAccessor => new HttpContextAccessor();
public static void AddBooksToContainer(IEnumerable<BookViewModel> books)
{
ContextAccessor.HttpContext.Session.Set(Key, books);
}
public static IEnumerable<BookViewModel> GetBooks()
{
return ContextAccessor.HttpContext.Session.Get<IEnumerable<BookViewModel>>(Key);
}
}
I have also added some extension methods to the session object :
public static class SessionExtensions
{
public static void Set<T>(this ISession session, string key, IEnumerable<T> value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
public static T Get<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) :
JsonConvert.DeserializeObject<T>(value);
}
}
Update: This is how i try to mock :
var bookList = new List<BookViewModel>()
{
new BookViewModel {Author = "test", Id = Guid.NewGuid(), InStock = 1, Price = 1000, Title = "tes"}
};
var httpContextAccessor = new Mock<IHttpContextAccessor>().SetupAllProperties();
httpContextAccessor.Setup(x => x.HttpContext.Session.Get<IEnumerable<BookViewModel>>("test")).Returns(bookList);
And the error i get :
System.NotSupportedException : Invalid setup on an extension method: x =>
x.HttpContext.Session.Get<IEnumerable`1>("test")
The problem is I have tried many ways to mock session and its extension methods but I got no result yet. many ways work in .netframework and not core.
I use xunit for testing and Moq library to mock the objects. Please tell me how can I mock session and its extension methods. Appreciate it.
The problem is not related to .NET Core, is a limitation of the mocking library, you can't mock extension methods with Moq.
This might help: Mocking Extension Methods with Moq
Depend on abstractions and not concretions. Create a Factory method that will provide you with the IHttpContextAccessor abstraction.
public static class BookManager {
private const string Key = "Books";
public static Func<IHttpContextAccessor> ContextAccessor = () => new HttpContextAccessor();
public static void AddBooksToContainer(IEnumerable<BookViewModel> books) {
ContextAccessor().HttpContext.Session.Set(Key, books);
}
public static IEnumerable<BookViewModel> GetBooks() {
return ContextAccessor().HttpContext.Session.Get<IEnumerable<BookViewModel>>(Key);
}
}
This will allow you to replace the abstraction when testing with a mock.
//Arrange
var mock = mock.Of<IHttpContextAccessor>();
BookManager.ContextAccessor = () => {
return mock.Object;
};
//...
Better yet if this manager only has need of the session then do not expose more than it needs.
public static class BookManager {
private const string Key = "Books";
public static Func<ISession> Session = () => new HttpContextAccessor().HttpContext.Session;
public static void AddBooksToContainer(IEnumerable<BookViewModel> books) {
Session().Set(Key, books);
}
public static IEnumerable<BookViewModel> GetBooks() {
return Session().Get<IEnumerable<BookViewModel>>(Key);
}
}
So now only the explicit dependencies need to be mocked for tests.
According to ISession Extensions source code GetString and SetString are also extension methods
You will need to mock ISession.TryGetValue and ISession.Set
//Arrange
var mock = Mock.Of<ISession>();
BookManager.Session = () => {
return mock;
};
var value = Encoding.UTF8.GetBytes("[{some books json strings here}]");
Mock.Get(mock).Setup(_ => _.TryGetValue("Books", out value))
.Returns(true);
//Act
var books = BookManager.GetBooks();
//...
I am running a C# app with MongoDB 2.0 driver and ran into the following error when I ran all my tests:
System.InvalidOperationException : Class map for <TopClassName> has been frozen and no further changes are allowed.
System.AggregateException : One or more errors occurred. ----> MongoDB.Bson.BsonException : Unable to find a matching member to provide the value for parameter 'inBed'
However, running every single test one at a time gives no errors.
So for some reason the class map is frozen...
Here is what I have
(P.S, if you spot something that is way off, let me know):
A hierarchy with 3 abstract classes and one concrete class:
public abstract class BaseEvent
{
[BsonId]
public Guid Id;
public List<int> Data;
public dynamic Condition;
public string TimeStamp;
[BsonConstructor]
protected BaseEvent(List<int> data, dynamic condition)
{
Data = data;
Condition = condition;
TimeStamp = DateTime.Now.ToString();
Id = Guid.NewGuid();
}
}
public abstract class Basic : BaseEvent
{
[BsonConstructor]
protected Basic(List<int> data, bool condition) : base(data, condition)
{}
}
public abstract class BedEvent : Basic
{
[BsonConstructor]
protected BedEvent(List<int> data, bool inBed) : base(data, inBed)
{}
}
public class DummyBed : BedEvent
{
[BsonConstructor]
public DummyBed(List<int> data, bool inBed) : base(data, inBed)
{
RegisterHelper.RegisterNewClass(this);
}
}
The RegisterHelper is a singleton that is kept alive, more on that a bit down.
The constructor of my app will register the top class BaseEvent:
var baseEvent = BsonClassMap.RegisterClassMap<BaseEvent>(cm =>
{
cm.SetIsRootClass(true);
cm.MapMember(c => c.Data);
cm.MapMember(c => c.Condition);
cm.MapMember(c => c.TimeStamp);
cm.MapIdMember(c => c.Id).SetIdGenerator(GuidGenerator.Instance);
});
RegisterHelper.ClassRegister.Add(baseEvent);
For each concrete class, as the DummyBed, I call a homebrewed (and likely to be the cause of the problem) register method.
This method looks at all the baseclasses and, if not found on a list, adds them recursively to all classes above them self, much like this:
public static void RegisterNewClass<T>(T theObject)
{
...
if (!lvl3Found)
{
var lvl3Map = new BsonClassMap(lvl3Type); //lvl3Type is a System.Type
lvl3Map.SetDiscriminator(lvl3Type.Name);
ClassRegister.Add(lvl3Map);
BsonClassMap.RegisterClassMap(lvl3Map);
lvl4Map.AddKnownType(lvl3Type);
}
...
}
Full class can be found here.
Running tests:
[TestFixture]
public class InsertEventIntoDatabaseTest
{
private EventDatabase _eventDatabase;
[SetUp]
public void Setup()
{
_eventDatabase = new EventDatabase();
_eventDatabase.EmptyDatabase(); //Clean slate each time
}
[TearDown]
public void TearDown()
{
_eventDatabase = null;
}
[Test]
public void GetSubTypeDocument_FindDummyBed_Success()
{
var bed = new DummyBed(new List<int>() { 1, 2, 3}, true);
_eventDatabase.InsertEventInDatabase(bed);
var doc = _eventDatabase.GetDocument();
_eventDatabase.GetSubTypeDocument(typeof(DummyBed));
Assert.That(doc, Is.TypeOf<DummyBed>()); //Great success
}
[Test]
public void FindTypeEvents_FindTwo_Succes()
{
var data = new List<int>() { 1, 2, 3 };
var bed1 = new DummyBed(data, true);
var bed2 = new DummyBed(data, true);
_eventDatabase.InsertEventInDatabase(bed1);
_eventDatabase.InsertEventInDatabase(bed2);
var foundEvents = _eventDatabase.FindTypeEvents(typeof(BedEvent));
Assert.That(foundEvents.Count, Is.EqualTo(2)); // Frozen
}
...
}
However - running each test by it self gives only green check marks. So it is down to how fast it can handle events.
The database is cleared each time a new test is run, so there should be no operations running.
The methods called should wait until they finish:
public void InsertEventInDatabase(BaseEvent inputBaseEvent)
{
inputBaseEvent.Condition = (inputBaseEvent.Condition is bool ? (inputBaseEvent.Condition == true ? 100 : 0) : inputBaseEvent.Condition);
var collection = _database.GetCollection<dynamic>(DatabaseCollection);
collection.InsertOneAsync(inputBaseEvent).Wait(); //Should wait, right?
}
public List<BaseEvent> FindTypeEvents(Type typeFilter)
{
var name = _database.GetCollection<BaseEvent>(DatabaseCollection)
.Find(x => x.Id != Guid.Empty)
.ToListAsync();
return name.Result; //Should wait here as well, right?
}
Any suggestions on where to look for the cause of the problem?
It is quite limit with information on the interwebs after the April update of the C# driver, so any suggestions are welcome.
So, the error did not go away, but apparently did not affect the system if ignored.
I believe this is a mistake, but I have 34 green unit tests now, and the system runs nicely in release mode.
public static void RegisterNewClass<T>(T theObject)
{
...
if (!lvl3Found)
{
try
{
lvl3Map.SetDiscriminator(lvl3Type.Name);
BsonClassMap.RegisterClassMap(lvl3Map);
lvl4Map.AddKnownType(lvl3Type);
}
catch (Exception e)
{
Console.WriteLine("Level 3 adding went wrong!");
Console.WriteLine(e.Message);
}
}
...
}
I can't say why it actually works.
What triggers the exception is the line lvl4Map.AddKnownType(lvl3Type) stating lvl4Map is frozen.
But as said, the test still runs with a green flag, so I am not sure it is a big deal.
It could be a bug.