Saga data containing IList of enum

sql-persistence
nservicebus
sagas

(Christopher Murphy) #1

Hi,

I have a saga that has data which contains a property with a list of enum values. I am getting “Could not determine type for: System.Collections.Generic.IList” exception on startup of the service.

As far as possible I have use as much convention and out of the box functionality (i.e. using endpointConfiguration.UsePersistence(); and connectionstring convention names) so have no custom nhconfig object.

From what I have read around this I am guessing have to use custom mapping (fluent, xml or annotation) but am struggling to find an solid examples for my case or explanation of how to do this and was wondering whether anyone could help please?

Many Thanks
Chris


(Ramon Smits) #2

@christopher_murphy Can you share the code of the saga data class but also your NHibernate mapping?


(Christopher Murphy) #3

Thanks for your reply. I have included the data class below.

public class ProposalsSagaData : ContainSagaData
{
    public virtual int ClientReferenceNumber { get; set; }
    public virtual IList<DebtActivityTrack> DebtActivities { get; set; }
    public virtual bool TimeoutRequested { get; set; }

    public ProposalsSagaData()
    {
        DebtActivities = new List<DebtActivityTrack>();
    }
}

public class DebtActivityTrack
{
    public virtual int DebtIdentifier { get; set; }
    public virtual IList<DebtActivities> Activities { get; set; }
}

public enum DebtActivities
{
    AccountNumberChange = 0,
    BalanceChange = 1,
    Reinstated = 2,
    Cleared = 3,
    PaymentChange = 4,
    ContractualPaymentChange = 5,
    Transferred = 6,
    New = 7
}

The area it falls over on startup is with the “Activities” property in the DebtActivityTrack class due to the IList of DebtActivities.

I do not have any explicit data mapping as of yet, leaving this to default mapping behavior. The only relevant config I have is below and leave connectionstrings to get picked up by naming convention (i.e. NServiceBus/Persistence/NHibernate/Saga):

endpointConfiguration.UsePersistence<NHibernatePersistence>();

(Tim Bussmann) #4

@christopher_murphy have you tried whether using List<T> instead of IList<T> would solve the problem?


(Ramon Smits) #5

I tried with a HashSet<DebtActivities> and that worked, using ICollection resulted in the following exception:

NHibernate.MappingException: Could not determine type for: System.Collections.Generic.ICollection`1[[DebtActivities, Server.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e, for columns: NHibernate.Mapping.Column(Activities)
   at NHibernate.Mapping.SimpleValue.get_Type()
   at NHibernate.Tuple.PropertyFactory.BuildStandardProperty(Property property, Boolean lazyAvailable)
   at NHibernate.Tuple.Component.ComponentMetamodel..ctor(Component component)
   at NHibernate.Mapping.Component.BuildType()
   at NHibernate.Mapping.Component.get_Type()
   at NHibernate.Mapping.SimpleValue.IsValid(IMapping mapping)
   at NHibernate.Mapping.Collection.Validate(IMapping mapping)
   at NHibernate.Cfg.Configuration.ValidateCollections()
   at NHibernate.Cfg.Configuration.Validate()
   at NHibernate.Cfg.Configuration.BuildSessionFactory()
   at NServiceBus.Features.NHibernateStorageSession.Setup(FeatureConfigurationContext context) in C:\BuildAgent\work\4caed983f8dafcd5\src\NServiceBus.NHibernate\SynchronizedStorage\NHibernateStorageSession.cs:line 58
   at NServiceBus.Features.FeatureActivator.ActivateFeature(FeatureInfo featureInfo, List`1 featuresToActivate, IConfigureComponents container, PipelineSettings pipelineSettings, RoutingComponent routing, ReceiveConfiguration receiveConfiguration) in C:\BuildAgent\work\ed946b9f0e4aae01\src\NServiceBus.Core\Features\FeatureActivator.cs:line 193
   at NServiceBus.Features.FeatureActivator.SetupFeatures(IConfigureComponents container, PipelineSettings pipelineSettings, RoutingComponent routing, ReceiveConfiguration receiveConfiguration) in C:\BuildAgent\work\ed946b9f0e4aae01\src\NServiceBus.Core\Features\FeatureActivator.cs:line 57
   at NServiceBus.InitializableEndpoint.Initialize() in C:\BuildAgent\work\ed946b9f0e4aae01\src\NServiceBus.Core\InitializableEndpoint.cs:line 54
   at NServiceBus.Endpoint.Start(EndpointConfiguration configuration) in C:\BuildAgent\work\ed946b9f0e4aae01\src\NServiceBus.Core\Endpoint.cs:line 27
   at Program.Main() in S:\particular\docs.particular.net\samples\nhibernate\simple\NHibernate_8\Server\Program.cs:line 35
   at Program.<Main>()

Interestingly using ICollection<DebtActivities> did work on the aggregate root (ProposalsSagaData).

Looking further in the created schema I see that activities is mapped to a varbinary(max) which seems to not use a class mapping for the Activities property but uses binary serialization. This likely because DebtActivityTrack is not an NHibernate entity due to a missing Id property.

Solutions:

Use a flagged enum

Instead of using an enum you could maybe use a flagged enum and not use a collection. This results in an int column but would require you to use enum operators in code

public class DebtActivityTrack
{
    public virtual int DebtIdentifier { get; set; }
    public virtual DebtActivities Activities { get; set; }
}

[Flags]
public enum DebtActivities
{
    AccountNumberChange = 1,// Notice the values require to be power of 2
    BalanceChange = 2,
    Reinstated = 4,
    Cleared = 8,
    PaymentChange = 16,
    ContractualPaymentChange = 32,
    Transferred = 64,
    New = 128
}

Convert DebtActivityTrack to an entity

You could make DebtActivityTrack an entity by adding an Id property which I recommend anyway to do as without it every read/write will DELETE/INSERT all collection items. As this now becomes an entity the enum will be mapped to its own table.

public class ProposalsSagaData : ContainSagaData
{
    public virtual int ClientReferenceNumber { get; set; }
    public virtual IList<DebtActivityTrack> DebtActivities { get; set; } // Potentially this can be an ICollection too, I don't think list order is relevant but would require a custom mapping.
    public virtual bool TimeoutRequested { get; set; }

    public ProposalsSagaData()
    {
        DebtActivities = new List<DebtActivityTrack>();
    }
}

public class DebtActivityTrack
{
    public virtual Guid Id { get; set; } // Added Id property to make it an entity
    public virtual int DebtIdentifier { get; set; }
    public virtual ICollection<DebtActivities> Activities { get; set; } = new HashSet<DebtActivities>(); // Converted to ICollection/HashSet as I think you would not allow the same enum value to be there more than once.
}

public enum DebtActivities
{
    AccountNumberChange = 0,
    BalanceChange = 1,
    Reinstated = 2,
    Cleared = 3,
    PaymentChange = 4,
    ContractualPaymentChange = 5,
    Transferred = 6,
    New = 7
}