/// <summary> /// Creates a <see cref="SagaMetadata" /> from a specific Saga type. /// </summary> /// <param name="sagaType">A type representing a Saga. Must be a non-generic type inheriting from <see cref="Saga" />.</param> /// <param name="availableTypes">Additional available types, used to locate saga finders and other related classes.</param> /// <param name="conventions">Custom conventions to use while scanning types.</param> /// <returns>An instance of <see cref="SagaMetadata" /> describing the Saga.</returns> public static SagaMetadata Create(Type sagaType, IEnumerable <Type> availableTypes, Conventions conventions) { Guard.AgainstNull(nameof(sagaType), sagaType); Guard.AgainstNull(nameof(availableTypes), availableTypes); Guard.AgainstNull(nameof(conventions), conventions); if (!IsSagaType(sagaType)) { throw new Exception(sagaType.FullName + " is not a saga"); } var genericArguments = GetBaseSagaType(sagaType).GetGenericArguments(); if (genericArguments.Length != 1) { throw new Exception($"'{sagaType.Name}' saga type does not implement Saga<T>"); } var saga = (Saga)FormatterServices.GetUninitializedObject(sagaType); var mapper = new SagaMapper(); saga.ConfigureHowToFindSaga(mapper); var sagaEntityType = genericArguments.Single(); ApplyScannedFinders(mapper, sagaEntityType, availableTypes, conventions); var finders = new List <SagaFinderDefinition>(); var propertyMappings = mapper.Mappings.Where(m => !m.HasCustomFinderMap) .GroupBy(m => m.SagaPropName) .ToList(); if (propertyMappings.Count > 1) { var messageTypes = string.Join(",", propertyMappings.SelectMany(g => g.Select(m => m.MessageType.FullName)).Distinct()); throw new Exception($"Sagas can only have mappings that correlate on a single saga property. Use custom finders to correlate {messageTypes} to saga {sagaType.Name}"); } CorrelationPropertyMetadata correlationProperty = null; if (propertyMappings.Any()) { var mapping = propertyMappings.Single().First(); correlationProperty = new CorrelationPropertyMetadata(mapping.SagaPropName, mapping.SagaPropType); } var associatedMessages = GetAssociatedMessages(sagaType) .ToList(); foreach (var mapping in mapper.Mappings) { var associatedMessage = associatedMessages.FirstOrDefault(m => mapping.MessageType.IsAssignableFrom(m.MessageType)); if (associatedMessage == null) { var msgType = mapping.MessageType.Name; if (mapping.HasCustomFinderMap) { throw new Exception($"Custom saga finder {mapping.CustomFinderType.FullName} maps message type {msgType} for saga {sagaType.Name}, but the saga does not handle that message. If {sagaType.Name} is supposed to handle this message, it should implement IAmStartedByMessages<{msgType}> or IHandleMessages<{msgType}>."); } throw new Exception($"Saga {sagaType.Name} contains a mapping for {msgType} in the ConfigureHowToFindSaga method, but does not handle that message. If {sagaType.Name} is supposed to handle this message, it should implement IAmStartedByMessages<{msgType}> or IHandleMessages<{msgType}>."); } SetFinderForMessage(mapping, sagaEntityType, finders); } foreach (var messageType in associatedMessages) { if (messageType.IsAllowedToStartSaga) { var match = mapper.Mappings.FirstOrDefault(m => m.MessageType.IsAssignableFrom(messageType.MessageType)); if (match == null) { var simpleName = messageType.MessageType.Name; throw new Exception($"Message type {simpleName} can start the saga {sagaType.Name} (the saga implements IAmStartedByMessages<{simpleName}>) but does not map that message to saga data. In the ConfigureHowToFindSaga method, add a mapping using:{Environment.NewLine} mapper.ConfigureMapping<{simpleName}>(message => message.SomeMessageProperty).ToSaga(saga => saga.MatchingSagaProperty);"); } } } return(new SagaMetadata(sagaType.FullName, sagaType, sagaEntityType.FullName, sagaEntityType, correlationProperty, associatedMessages, finders)); }
/// <summary> /// Initializes a new instance of the <see cref="SagaMetadata" /> class. /// </summary> /// <param name="name">The name of the saga.</param> /// <param name="sagaType">The type for this saga.</param> /// <param name="entityName">The name of the saga data entity.</param> /// <param name="sagaEntityType">The type of the related saga entity.</param> /// <param name="correlationProperty">The property this saga is correlated on if any.</param> /// <param name="messages">The messages collection that a saga handles.</param> /// <param name="finders">The finder definition collection that can find this saga.</param> public SagaMetadata(string name, Type sagaType, string entityName, Type sagaEntityType, CorrelationPropertyMetadata correlationProperty, IReadOnlyCollection <SagaMessage> messages, IReadOnlyCollection <SagaFinderDefinition> finders) { this.correlationProperty = correlationProperty; Name = name; EntityName = entityName; SagaEntityType = sagaEntityType; SagaType = sagaType; if (!messages.Any(m => m.IsAllowedToStartSaga)) { throw new Exception($@" Sagas must have at least one message that is allowed to start the saga. Add at least one `IAmStartedByMessages` to the {name} saga."); } if (correlationProperty != null) { if (!AllowedCorrelationPropertyTypes.Contains(correlationProperty.Type)) { var supportedTypes = string.Join(",", AllowedCorrelationPropertyTypes.Select(t => t.Name)); throw new Exception($@" {correlationProperty.Type.Name} is not supported for correlated properties. Change the correlation property {correlationProperty.Name} on saga {name} to any of the supported types, {supportedTypes}, or use a custom saga finder."); } } associatedMessages = new Dictionary <string, SagaMessage>(); foreach (var sagaMessage in messages) { associatedMessages[sagaMessage.MessageTypeName] = sagaMessage; } sagaFinders = new Dictionary <string, SagaFinderDefinition>(); foreach (var finder in finders) { sagaFinders[finder.MessageTypeName] = finder; } }
/// <summary> /// Property this saga is correlated on. /// </summary> public bool TryGetCorrelationProperty(out CorrelationPropertyMetadata property) { property = correlationProperty; return(property != null); }