MassTransit Testing Saga StateMachine - The state machine was not properly configured - c#

I am trying to test / execute a State Machine Saga from MassTransit but I am receiving this error
Message:
MassTransit.ConfigurationException : Failed to create the state machine connector for Invoices.Features.EntryInvoices.ProcessEntryInvoiceSagaState
----> MassTransit.ConfigurationException : The state machine was not properly configured:
[Failure] ProcessInvoiceReceived was not specified
Have tried to search on the documentation but didn't find anything related. Don't know where I am missing, maybe saga configuration or something els.
here is my Saga:
using ...;
namespace Invoices.Features.EntryInvoices
{
public class ProcessEntryInvoiceSaga:MassTransitStateMachine<ProcessEntryInvoiceSagaState>
{
public State Started { get; private set; }
public State Processing { get; private set; }
public State Closed { get; private set; }
public Event<ProcessInvoice> ProcessInvoiceReceived { get; private set; }
public Event<ProductsAdded> ProductsAddedEvent { get; private set; }
public Event<DuplicatesRegistered> DuplicatesRegisteredEvent { get; private set; }
public ProcessEntryInvoiceSaga()
{
InstanceState(x => x.CurrentState);
Event(() => ProcessInvoiceReceived);
Event(() => ProductsAddedEvent);
Event(() => DuplicatesRegisteredEvent);
Initially(
When(ProcessInvoiceReceived)
.Then(x => x.Saga.InvoiceId = x.Message.InvoiceId)
.Activity(x=> x.OfType<ProcessEntryInvoiceStartActivity>())
.TransitionTo(Started)
);
During(Started,
When(ProductsAddedEvent)
.TransitionTo(Processing),
When(DuplicatesRegisteredEvent)
.TransitionTo(Processing));
During(Processing,
When(ProductsAddedEvent)
.TransitionTo(Closed),
When(DuplicatesRegisteredEvent)
.TransitionTo(Closed));
WhenEnter(Closed, binder => binder
.Activity(x=>x.OfType<ProcessEntryInvoiceFinishActivity>())
);
SetCompleted(async instance =>
{
State currentState = await this.GetState(instance);
return Closed.Equals(currentState);
});
}
}
public class ProcessEntryInvoiceSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public long InvoiceId { get; set; }
}
}
And the test that I am trying to execute:
using ...;
namespace Invoices.Test.Features
{
public class ProcessEntryInvoiceSagaTest
{
Mock<IEntryInvoiceRepository> _entryInvoiceRepository;
ITestHarness _harness;
[SetUp]
public async Task Setup()
{
_entryInvoiceRepository = new Mock<IEntryInvoiceRepository>();
var provider = new ServiceCollection()
.AddSingleton(_entryInvoiceRepository.Object)
.AddSingleton<ISagaRepository<ProcessEntryInvoiceSagaState>, InMemorySagaRepository<ProcessEntryInvoiceSagaState>>()
.AddMassTransitTestHarness(cfg =>
{
cfg.AddSagaStateMachine<ProcessEntryInvoiceSaga, ProcessEntryInvoiceSagaState>().InMemoryRepository();
cfg.AddConsumer<MockSagaStepConsumers>();
})
.BuildServiceProvider(true);
_harness = provider.GetRequiredService<ITestHarness>();
await _harness.Start();
}
[Test]
public async Task TestSetNewProductItemWithSuccess()
{
var openedInvoice = new EntryInvoice() {...};
var form = new ProcessInvoice()
{
InvoiceId = 123
};
await _harness.Bus.Publish(form);
Assert.That(await _harness.Consumed.Any<ProcessInvoice>());
Assert.That(await _harness.Published.Any<AddProductsToInventory>());
Assert.That(await _harness.Published.Any<RegisterDuplicatesOfInvoice>());
Assert.That(await _harness.Consumed.Any<DuplicatesRegistered>());
Assert.That(await _harness.Consumed.Any<ProductsAdded>());
Assert.That(await _harness.Published.Any<EntryInvoiceClosed>());
_entryInvoiceRepository.Verify(i => i.Update(It.IsAny<EntryInvoice>()), Times.AtLeast(2));
_entryInvoiceRepository.Verify(i => i.SaveChanges(), Times.AtLeast(2));
openedInvoice.Status.Should().Be(InvoiceStatus.Closed);
}
}
public class MockSagaStepConsumers : IConsumer<AddProductsToInventory>, IConsumer<RegisterDuplicatesOfInvoice>
{
public async Task Consume(ConsumeContext<AddProductsToInventory> context)
{
await context.Publish(new ProductsAdded()
{
CorrelationId = context.MessageId ?? Guid.Empty
});
}
public async Task Consume(ConsumeContext<RegisterDuplicatesOfInvoice> context)
{
await context.Publish(new DuplicatesRegistered()
{
CorrelationId = context.MessageId ?? Guid.Empty,
InvoiceId = context.Message.InvoiceId
});
}
}
}

Found the issue on
Event(() => ProcessInvoiceReceived);
Event(() => ProductsAddedEvent);
Event(() => DuplicatesRegisteredEvent);
The documentation says that the events are autoconfigured but only if they have CorrelationId or CorrelatedBy<Guid> interface.
I just added the interface CorrelatedBy<Guid> to my messages and set a CorrelationID to the first message and this allowed me to continue the process.

Related

Create Object Using Constructor and Argument with AutoMapper

I have a scenario where I want to create an object (Calc) which takes some options as a constructor argument. It happens I have a serialized version of Calc, its properties and the options properties as another single object. Here's my code:
void Main()
{
var mockMapper = new MapperConfiguration(c =>
{
c.AddProfile<MapperProf>();
})
.CreateMapper();
}
public class MapperProf : Profile
{
public MapperProf()
{
CreateMap<Scen, Calc>()
.ConstructUsing(c => new Calc()) // I AM STUCK HERE
CreateMap<Scen, Opt>()
.ForMember(o => o.OptProp, o => o.MapFrom(o => o.OptProp));
}
}
public class Calc
{
public Calc(Opt opt)
{
OptProp = opt.OptProp;
}
public string CalcProp { get; set; }
private string OptProp { get; set; }
}
public class Opt
{
public string OptProp { get; set; }
}
public class Scen
{
public string CalcProp { get; set; }
public string OptProp { get; set; }
}
For various reasons I cannot access Calc.OptProp, I have to pass it in via the constructor argument.
In equivalent terms what I want to do in one shot is:
Calc c = mockMapper.Map<Calc>().ConstructUsing(c => new Calc(mockMapper.Map<Opt>(scen)));
That is, construct both the Calc and Opt from the same Scen.
In the ConstructUsing you can use ResulotionContext and then use mapper for create constructor like this:
public class MapperProf : Profile
{
public MapperProf()
{
CreateMap<Scen, Opt>().ForMember(o => o.OptProp, o => o.MapFrom(scen => scen.OptProp));
CreateMap<Scen, Calc>().ConstructUsing((scen, context) => new Calc(context.Mapper.Map<Scen, Opt>(scen)));
}
};
And for mapping scen:
var scen = new Scen() { CalcProp = "Calc", OptProp = "Opt" };
var calc = mockMapper.Map<Scen, Calc>(scen);

MassTransit Automatonymous - State not changing when a message is Sent

I am trying to figure why "Sending" a message does not invoke state machine, but if I "Publish" a message, it works and I can see the state changing.
Following is my code, it is similar to the documentation, except that I am trying to "Send" a message.
Components
State Machine:
public class OrderState: SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public int CurrentState { get; set; }
public DateTime? OrderDate { get; set; }
}
public class OrderStateMachine: MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; }
public State Accepted { get; private set; }
public State Completed { get; private set; }
public Event<SubmitOrder> SubmitOrder { get; private set; }
public Event<OrderAccepted> OrderAccepted { get; private set; }
public Event<OrderCompleted> OrderCompleted { get; private set; }
public OrderStateMachine()
{
InstanceState(x => x.CurrentState, Submitted, Accepted, Completed);
Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId));
Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId));
Initially(
When(SubmitOrder)
.Then(context => context.Instance.OrderDate = context.Data.OrderDate)
.TransitionTo(Submitted));
During(Submitted,
When(OrderAccepted)
.TransitionTo(Accepted));
During(Accepted,
Ignore(SubmitOrder));
DuringAny(
When(OrderCompleted)
.TransitionTo(Completed));
SetCompleted(async instance =>
{
var currentState = await this.GetState(instance);
return Completed.Equals(currentState);
});
}
}
Contracts:
public record SubmitOrder(Guid OrderId, DateTime? OrderDate);
public record OrderAccepted(Guid OrderId);
public record OrderCompleted(Guid OrderId);
Consumers:
public class SubmitOrderConsumer: IConsumer<SubmitOrder>
{
public async Task Consume(ConsumeContext<SubmitOrder> context)
{
await Task.Delay(2000);
}
}
public class SubmitOrderConsumerDefinition : ConsumerDefinition<SubmitOrderConsumer>
{
public SubmitOrderConsumerDefinition()
{
EndpointName = "submit-order";
}
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<SubmitOrderConsumer> consumerConfigurator)
{
endpointConfigurator.ConfigureConsumeTopology = false;
}
}
Web API
Program.cs (snippet)
// Add services to the container.
builder.Services.AddMassTransit(cfg =>
{
cfg.SetKebabCaseEndpointNameFormatter();
cfg.UsingRabbitMq((context, configurator) =>
{
configurator.Host("localhost", "/", hostConfigurator =>
{
hostConfigurator.Username("guest");
hostConfigurator.Password("guest");
});
});
});
builder.Services.AddMassTransitHostedService();
builder.Services.AddControllers();
OrderController
[Route("order")]
public class OrderController : ControllerBase
{
private readonly ISendEndpointProvider _sendEndpointProvider;
public OrderController(ISendEndpointProvider sendEndpointProvider)
{
_sendEndpointProvider = sendEndpointProvider;
}
[HttpPost]
public async Task<IActionResult> SendOrder()
{
var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("exchange:submit-order"));
await endpoint.Send(new SubmitOrder(Guid.NewGuid(), DateTime.Now));
return Ok();
}
}
Worker Service
Program.cs
using IHost = Microsoft.Extensions.Hosting.IHost;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddMassTransit(cfg =>
{
cfg.AddConsumer<SubmitOrderConsumer>(typeof(SubmitOrderConsumerDefinition));
cfg.AddSagaStateMachine<OrderStateMachine, OrderState>().InMemoryRepository();
cfg.UsingRabbitMq((context, rabbitMqConfigurator) =>
{
rabbitMqConfigurator.Host("localhost", "/", hostConfigurator =>
{
hostConfigurator.Username("guest");
hostConfigurator.Password("guest");
});
rabbitMqConfigurator.ReceiveEndpoint("saga-order", endpointConfigurator =>
{
endpointConfigurator.ConfigureSaga<OrderState>(context);
});
rabbitMqConfigurator.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
Then I do a POST via Postman to: http://localhost:5000/order
It does call the SubmitOrderConsumer, but for some reason, the State machine does not get invoked (it won't hit breakpoint inside the Then handler that sets the Order Date inside Initially state.). I think I am missing something that connects the two together.
Any feedback is greatly appreciated. Thank you.
In your example, you'd want to use Publish, especially in this scenario where you have two consumers (the consumer, and the state machine) on separate endpoints (queue) that would be consuming the message. Sending directly to the exchange would only get the message to one of the endpoints.

MassTransit messages types must not be System types exception

I'm pretty new to MassTransit and don't understand what am I doing wrong to get the following exception: Messages types must not be System types.
Here are my definitions:
[BsonIgnoreExtraElements]
public class ArcProcess : SagaStateMachineInstance, ISagaVersion
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public int Version { get; set; }
public Guid ActivationId { get; set; }
}
public static class MessageContracts
{
static bool _initialized;
public static void Initialize()
{
if (_initialized)
return;
GlobalTopology.Send.UseCorrelationId<StartProcessingMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ReconstructionFinishedMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ProcessingFinishedMessage>(x => x.ActivationId);
_initialized = true;
}
}
2 of my consumers are:
public class StartReconstructionConsumer : IConsumer<StartProcessingMessage>
{
readonly ILogger<StartReconstructionConsumer> _Logger;
private readonly int _DelaySeconds = 5;
public StartReconstructionConsumer(ILogger<StartReconstructionConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<StartProcessingMessage> context)
{
var activationId = context.Message.ActivationId;
_Logger.LogInformation($"Received Scan: {activationId}");
await Task.Delay(_DelaySeconds * 1000);
_Logger.LogInformation($"Finish Scan: {activationId}");
await context.Publish<ReconstructionFinishedMessage>(new { ActivationId = activationId });
}
}
public class ProcessingFinishedConsumer : IConsumer<ProcessingFinishedMessage>
{
readonly ILogger<ProcessingFinishedConsumer> _Logger;
public ProcessingFinishedConsumer(ILogger<ProcessingFinishedConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<ProcessingFinishedMessage> context)
{
_Logger.LogInformation($"Finish {context.Message.ActivationId}");
await Task.CompletedTask;
}
}
And here is the StateMachine definition:
public class ArcStateMachine: MassTransitStateMachine<ArcProcess>
{
static ArcStateMachine()
{
MessageContracts.Initialize();
}
public ArcStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(ProcessingStartedEvent)
.Then(context =>
{
Console.WriteLine(">> ProcessingStartedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.TransitionTo(ProcessingStartedState));
During(ProcessingStartedState,
When(ReconstructionFinishedEvent)
.Then(context =>
{
Console.WriteLine(">> ReconstructionFinishedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.Publish(context =>
{
return context.Init<ProcessingFinishedMessage>(new { ActivationId = context.Data.ActivationId });
})
.TransitionTo(ProcessingFinishedState)
.Finalize());
}
public State ProcessingStartedState { get; }
public State ReconstructionStartedState { get; }
public State ReconstructionFinishedState { get; }
public State ProcessingFinishedState { get; }
public Event<StartProcessingMessage> ProcessingStartedEvent { get; }
public Event<ReconstructionStartedMessage> ReconstructionStartedEvent { get; }
public Event<ReconstructionFinishedMessage> ReconstructionFinishedEvent { get; }
public Event<ProcessingFinishedMessage> ProcessingFinishedEvent { get; }
}
And the setup for MassTransit looks the following:
var rabbitHost = Configuration["RABBIT_MQ_HOST"];
if (rabbitHost.IsNotEmpty())
{
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
var machine = new ArcStateMachine();
var repository = MongoDbSagaRepository<ArcProcess>.Create(connectionString,
"mongoRepo", "WorkflowState");
cnf.AddConsumer(typeof(StartReconstructionConsumer));
cnf.AddConsumer(typeof(ProcessingFinishedConsumer));
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
cfg.ReceiveEndpoint(BusConstants.SagaQueue,
e => e.StateMachineSaga(machine, repository));
});
});
services.AddMassTransitHostedService();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApp", Version = "v1" });
});
}
I have several questions about it:
When actually the event is published as a result of publishing a message? I.e. in my example await _BusInstance.Bus.Publish<StartProcessingMessage>(new { ActivationId = id }); is called from a WebApi which is consumed by StartReconstructionConsumer but when actually the state machine starts to act with Initially(When(ProcessingStartedEvent)...?
My processing should ensure I'm already in the ProcessingStartedState state in order to During(ProcessingStartedState, When(ReconstructionFinishedEvent)... to act correctly. So how do I ensure that my consumer that fires upon receive of StartProcessingMessage can publish the ReconstructionFinishedMessage that should initiate that During? Am I building the messages exchange correctly?
Currently for the await context.Publish<ReconstructionFinishedMessage>(new { ActivationId = activationId }); I get an exception in the logs that states R-FAULT rabbitmq://localhost/saga.service d4070000-7b3b-704d-0f10-08d99942c959 Nanox.GC.Shared.AppCore.Messages.ReconstructionFinishedMessage ReconCaller.Saga.ArcProcess(00:00:04.1132604) while that guid in the message is actually the MessageId. And my message in the rabbitmq is routed to saga.service_error with an exception Messages types must not be System types: System.Threading.Tasks.Task<Nanox.GC.Shared.AppCore.Messages.ProcessingFinishedMessage> (Parameter 'T').
It seems like I'm missing here really big..
My intent is to initiate processing that will have several stages processed by a few consumers sequentially. So here I tried to build a simple StateMachine that starts whenever someone called StartProcessing, then each consumer will do its job and fire the FinishedStepX which will promote the state machine to a new step and initiate the next consumer up until all the processing is done and the state machine will report ProcessingComplete.
Thanks for any help n advance
First, your bus configuration is a bit strange, so I've cleaned that up:
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
cfg.AddSagaStateMachine<ArcStateMachine, ArcProcess>()
.Endpoint(e => e.Name = BusConstants.SagaQueue)
.MongoDbRepository(connectionString, r =>
{
r.DatabaseName = "mongoRepo";
r.CollectionName = "WorkflowState";
});
cnf.AddConsumer<StartReconstructionConsumer>();
cnf.AddConsumer<ProcessingFinishedConsumer>();
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
});
});
And the publish issue is related to the method being used, only PublishAsync allows the use of message initializers:
During(ProcessingStartedState,
When(ReconstructionFinishedEvent)
.Then(context =>
{
Console.WriteLine(">> ReconstructionFinishedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.PublishAsync(context =>
{
return context.Init<ProcessingFinishedMessage>(new { ActivationId = context.Data.ActivationId });
})
.TransitionTo(ProcessingFinishedState)
.Finalize());
That should sort you out.
With the generous help of #Chris Patterson the working solution would be:
Definitions:
[BsonIgnoreExtraElements]
public class ArcProcess : SagaStateMachineInstance, ISagaVersion
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public int Version { get; set; }
public Guid ActivationId { get; set; }
}
public interface StartProcessingMessage
{
Guid ActivationId { get; }
}
public interface ProcessingFinishedMessage
{
Guid ActivationId { get; }
}
public static class MessageContracts
{
static bool _initialized;
public static void Initialize()
{
if (_initialized)
return;
GlobalTopology.Send.UseCorrelationId<StartProcessingMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ProcessingFinishedMessage>(x => x.ActivationId);
_initialized = true;
}
}
Consumers:
public class StartProcessingConsumer : IConsumer<StartProcessingMessage>
{
readonly ILogger<StartProcessingConsumer> _Logger;
private readonly int _DelaySeconds = 5;
public StartProcessingConsumer(ILogger<StartProcessingConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<StartProcessingMessage> context)
{
var activationId = context.Message.ActivationId;
_Logger.LogInformation($"Received Scan: {activationId}");
await Task.Delay(_DelaySeconds * 1000);
_Logger.LogInformation($"Finish Scan: {activationId}");
await context.Publish<ProcessingFinishedMessage>(new { ActivationId = activationId });
}
}
public class ProcessingFinishedConsumer : IConsumer<ProcessingFinishedMessage>
{
readonly ILogger<ProcessingFinishedConsumer> _Logger;
public ProcessingFinishedConsumer(ILogger<ProcessingFinishedConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<ProcessingFinishedMessage> context)
{
_Logger.LogInformation($"Finish {context.Message.ActivationId}");
await Task.CompletedTask;
}
}
StateMachine definition:
public class ArcStateMachine: MassTransitStateMachine<ArcProcess>
{
static ArcStateMachine()
{
MessageContracts.Initialize();
}
public ArcStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(ProcessingStartedEvent)
.Then(context =>
{
context.Instance.ActivationId = context.Data.ActivationId;
})
.TransitionTo(ProcessingStartedState));
During(ProcessingStartedState,
When(ProcessingFinishedEvent)
.Then(context =>
{
context.Instance.ActivationId = context.Data.ActivationId;
})
.Finalize());
}
public State ProcessingStartedState { get; }
public State ProcessingFinishedState { get; }
public Event<StartProcessingMessage> ProcessingStartedEvent { get; }
public Event<ProcessingFinishedMessage> ProcessingFinishedEvent { get; }
}
MassTransit setup:
var rabbitHost = Configuration["RABBIT_MQ_HOST"];
if (rabbitHost.IsNotEmpty())
{
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
cnf.AddSagaStateMachine<ArcStateMachine, ArcProcess>()
.Endpoint(e => e.Name = BusConstants.SagaQueue)
.MongoDbRepository(connectionString, r =>
{
r.DatabaseName = "mongoRepo";
r.CollectionName = "WorkflowState";
});
cnf.AddConsumer(typeof(StartProcessingConsumer));
cnf.AddConsumer(typeof(ProcessingFinishedConsumer));
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApp", Version = "v1" });
});
}
This example helped me a lot in understanding how the basics of MassTrasit work.

"Unable to determine the serialization information for" error on MongoDB complex type equal to null filter

I am getting the below error while trying to run equal filter against null for a complex type object in MongoDB.Driver 2.0:
InvalidOperationException: Unable to determine the serialization information for
e => e.Deletion.
at MongoDB.Driver.ExpressionFieldDefinition2.Render(IBsonSerializer1 docume
ntSerializer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.SimpleFilterDefinition2.Render(IBsonSerializer1 documentS
erializer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.AndFilterDefinition1.Render(IBsonSerializer1 documentSeri
alizer, IBsonSerializerRegistry serializerRegistry)
at MongoDB.Driver.MongoCollectionImpl1.FindOneAndUpdateAsync[TProjection](Fi
lterDefinition1 filter, UpdateDefinition1 update, FindOneAndUpdateOptions2 op
tions, CancellationToken cancellationToken)
This is the filter:
Builders<TEntity>.Filter.Eq(e => e.Deletion, null)
In order to reproduce, run the following code with MongoDB.Driver 2.0.0 version:
public sealed class OccuranceWithReason
{
public OccuranceWithReason() : this(null)
{
}
public OccuranceWithReason(string reason)
{
Reason = reason;
OccuredOn = DateTime.UtcNow;
}
public string Reason { get; private set; }
public DateTime OccuredOn { get; private set; }
}
public interface IDeletable
{
OccuranceWithReason Deletion { get; }
}
public abstract class BaseEntity : IDeletable
{
protected BaseEntity()
{
Id = ObjectId.GenerateNewId().ToString();
}
public string Id { get; private set; }
public int PeekedCount { get; set; }
public OccuranceWithReason Deletion { get; private set; }
}
public class FooEntity : BaseEntity
{
}
class Program
{
static void Main(string[] args)
{
MongoConfig.Configure();
var client = new MongoClient();
var db = client.GetDatabase("foo");
var fooCol = db.GetCollection<FooEntity>("foos");
var foo = PeekForInsertSync(fooCol);
}
public static TEntity PeekForInsertSync<TEntity>(IMongoCollection<TEntity> collection)
where TEntity : BaseEntity
{
var query = Builders<TEntity>.Filter.And(
Builders<TEntity>.Filter.Eq(e => e.Deletion, null),
Builders<TEntity>.Filter.Lte(e => e.PeekedCount, 10)
);
return collection.Find(query).FirstOrDefaultAsync().Result;
}
}
internal static class MongoConfig
{
public static void Configure()
{
RegisterConventions();
RegisterGlobalSerializationRules();
ConfigureEntities();
ConfigureValueObjects();
}
private static void RegisterConventions()
{
var pack = new ConventionPack { new CamelCaseElementNameConvention(), new IgnoreIfNullConvention(false) };
ConventionRegistry.Register("all", pack, t => true);
}
private static void RegisterGlobalSerializationRules()
{
BsonSerializer.UseNullIdChecker = true;
}
private static void ConfigureEntities()
{
BsonClassMap.RegisterClassMap<BaseEntity>(cm =>
{
cm.MapMember(c => c.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.SetIdMember(cm.GetMemberMap(c => c.Id));
});
}
private static void ConfigureValueObjects()
{
BsonClassMap.RegisterClassMap<OccuranceWithReason>(cm =>
{
cm.AutoMap();
cm.MapCreator(occurance => new OccuranceWithReason(occurance.Reason));
});
}
}
Any idea?
The problem is about the BaseEntity object serialization registration. Deletion property was not mapped. Auto mapping all fields solved the problem:
BsonClassMap.RegisterClassMap<BaseEntity>(cm =>
{
cm.AutoMap();
cm.MapMember(c => c.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.SetIdMember(cm.GetMemberMap(c => c.Id));
});

Singleton with Entity Framework : Will Queries run multiple times?

With a model such as ...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.ComponentModel.DataAnnotations;
namespace Singleton
{
public class Program
{
public static void Main(string[] args)
{
var builder = new ModelBuilder();
builder.Configurations.Add(new TemplateConfiguration());
builder.Configurations.Add(new UserConfiguration());
builder.Configurations.Add(new UnitConfiguration());
builder.Configurations.Add(new AttributeConfiguration());
var model = builder.CreateModel();
using (var context = new SampleDataContext(model))
{
bool updating = true;
if (updating)
{
var units = new List<Unit>
{
new Unit{ Name = "Unit1" },
new Unit{ Name = "Unit2" }
};
units.ForEach(x => { context.Units.Add(x); });
context.SaveChanges();
var templates = new List<Template>
{
new Template{
Name = "Default",
Attributes = new List<Attribute>
{
new Attribute
{
Unit = context.Units.Single( i => i.Name == "Unit1" )
}
}
}
};
templates.ForEach(x =>
{
context.Templates.Add(x);
});
context.SaveChanges();
var users = new List<User>
{
new User
{
Name = "Stacey"
},
new User
{
Name = "Daniel"
},
new User
{
Name = "Derek"
}
};
users.ForEach(x => { context.Users.Add(x); });
context.SaveChanges();
updating = !updating; // stop updating
}
if (!updating)
{
Single.Instance = context.Templates.Single(i => i.Name == "Default");
}
foreach (User user in context.Users)
{
Console.WriteLine(user.Template.Name); // does template requery?
}
}
}
}
public class Template
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<Attribute> Attributes { get; set; }
}
public class TemplateConfiguration : EntityConfiguration<Template>
{
public TemplateConfiguration()
{
HasKey(k => k.Id);
Property(k => k.Id).IsIdentity();
Property(k => k.Name);
//// map the collection entity
HasMany(k => k.Attributes).WithRequired()
.Map("template.attributes",
(template, attribute) => new
{
Template = template.Id,
Attribute = attribute.Id
});
MapSingleType(c => new
{
c.Id,
c.Name
}).ToTable("templates");
}
}
public class User
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
[StoreGenerated(StoreGeneratedPattern.None)]
public Template Template { get { return Single.Instance; } }
}
public class UserConfiguration : EntityConfiguration<User>
{
public UserConfiguration()
{
HasKey(k => k.Id);
Property(k => k.Id).IsIdentity();
Property(k => k.Name);
MapSingleType(c => new
{
c.Id,
c.Name
}).ToTable("users");
}
}
public class Unit
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class UnitConfiguration : EntityConfiguration<Unit>
{
public UnitConfiguration()
{
HasKey(k => k.Id);
Property(k => k.Id).IsIdentity();
Property(k => k.Name);
MapSingleType(c => new
{
c.Id,
c.Name
}).ToTable("units");
}
}
public class Attribute
{
public virtual int Id { get; set; }
public Unit Unit { get; set; }
}
public class AttributeConfiguration : EntityConfiguration<Attribute>
{
public AttributeConfiguration()
{
HasKey(k => k.Id);
Property(k => k.Id).IsIdentity();
// Initialize the Statistic
HasRequired(k => k.Unit);
// map the data type to the context so that it can be queried.
MapHierarchy(c => new
{
c.Id,
Unit = c.Unit.Id
}).ToTable("attributes");
}
}
public class Single
{
public static Template Instance;
}
public class SampleDataContext : DbContext
{
public SampleDataContext(DbModel model)
: base(model)
{
this.ObjectContext.ContextOptions.LazyLoadingEnabled = true;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
public DbSet<Template> Templates { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Unit> Units { get; set; }
public DbSet<Attribute> Attributes { get; set; }
}
}
if I assign Singleton.Instance to a query from the DataContext, and then assign the Singleton.Instance to other objects in my code, will the SQL be run once, or each time it is accessed? Can anyone help me here to see if this pattern is going to save some SQL?
From context.SomeQuery, you're almost certainly just returning some kind of queryable object or other iterable (I'm not sure your example reflects how your EF solution is actually architected, I think some elements were lost for the sake of brevity). So, every time you access this singleton, you're just going to iterate over it (run the query) again.
I recommend you use a simple memoization revolving around 4.0 caching, try this.
The SQL will run each time you access it.
What you'r looking for is some kind of caching strategy.
Have a look at the following thread. I think it will help you: How to make Entity Framework cache some objects

Categories