public static SagaMetadata Create(Type sagaType, IEnumerable <Type> availableTypes, 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(string.Format("'{0}' saga type does not implement Saga<T>", sagaType)); } var sagaEntityType = genericArguments.Single(); var uniquePropertiesOnEntity = FindUniqueAttributes(sagaEntityType).ToList(); var mapper = new SagaMapper(); var saga = (Saga)FormatterServices.GetUninitializedObject(sagaType); saga.ConfigureHowToFindSaga(mapper); ApplyScannedFinders(mapper, sagaEntityType, availableTypes, conventions); var finders = new List <SagaFinderDefinition>(); foreach (var mapping in mapper.Mappings) { if (uniquePropertiesOnEntity.All(p => p.Name != mapping.SagaPropName)) { uniquePropertiesOnEntity.Add(new CorrelationProperty { Name = mapping.SagaPropName }); } SetFinderForMessage(mapping, sagaEntityType, finders); } var associatedMessages = GetAssociatedMessages(sagaType) .ToList(); var metadata = new SagaMetadata(associatedMessages, finders) { Name = sagaType.FullName, EntityName = sagaEntityType.FullName, CorrelationProperties = uniquePropertiesOnEntity, SagaEntityType = sagaEntityType, SagaType = sagaType }; return(metadata); }
static void ApplyScannedFinders(SagaMapper mapper, Type sagaEntityType, IEnumerable <Type> availableTypes, Conventions conventions) { var actualFinders = availableTypes.Where(t => typeof(IFinder).IsAssignableFrom(t)) .ToList(); foreach (var finderType in actualFinders) { foreach (var interfaceType in finderType.GetInterfaces()) { var args = interfaceType.GetGenericArguments(); if (args.Length != 2) { continue; } Type messageType = null; Type entityType = null; foreach (var type in args) { if (typeof(IContainSagaData).IsAssignableFrom(type)) { entityType = type; } if (conventions.IsMessageType(type) || type == typeof(object)) { messageType = type; } } if (entityType == null || messageType == null || entityType != sagaEntityType) { continue; } var existingMapping = mapper.Mappings.SingleOrDefault(m => m.MessageType == messageType); if (existingMapping != null) { existingMapping.CustomFinderType = finderType; } else { mapper.ConfigureCustomFinder(finderType, messageType); } } } }
static void ApplyScannedFinders(SagaMapper mapper, Type sagaEntityType, IEnumerable <Type> availableTypes, Conventions conventions) { var actualFinders = availableTypes.Where(t => typeof(IFinder).IsAssignableFrom(t) && t.IsClass) .ToList(); foreach (var finderType in actualFinders) { foreach (var interfaceType in finderType.GetInterfaces()) { var args = interfaceType.GetGenericArguments(); //since we don't want to process the IFinder type if (args.Length != 2) { continue; } var entityType = args[0]; if (entityType != sagaEntityType) { continue; } var messageType = args[1]; if (!conventions.IsMessageType(messageType)) { var error = $"A custom IFindSagas must target a valid message type as defined by the message conventions. Change '{messageType.FullName}' to a valid message type or add it to the message conventions. Finder name '{finderType.FullName}'."; throw new Exception(error); } var existingMapping = mapper.Mappings.SingleOrDefault(m => m.MessageType == messageType); if (existingMapping != null) { var bothMappingAndFinder = $"A custom IFindSagas and an existing mapping where found for message '{messageType.FullName}'. Either remove the message mapping for remove the finder. Finder name '{finderType.FullName}'."; throw new Exception(bothMappingAndFinder); } mapper.ConfigureCustomFinder(finderType, messageType); } } }
/// <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)); }