diff --git a/Bonsai.Core.Tests/ExpressionBuilderGraphTests.cs b/Bonsai.Core.Tests/ExpressionBuilderGraphTests.cs index fa6b02db..1bbd5345 100644 --- a/Bonsai.Core.Tests/ExpressionBuilderGraphTests.cs +++ b/Bonsai.Core.Tests/ExpressionBuilderGraphTests.cs @@ -228,6 +228,36 @@ public void Build_BranchPropertyMapping_AvoidMulticastExpression() Assert.IsFalse(visitor.HasPublishBranch); } + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void BuildObservable_InvalidWorkflowType_ThrowsArgumentException() + { + new TestWorkflow() + .AppendValue(0) + .AppendOutput() + .BuildObservable(); + } + + [TestMethod] + public void BuildObservable_CovariantWorkflowType_IsCompatibleAssignment() + { + var workflow = new TestWorkflow() + .AppendValue("") + .AppendOutput() + .BuildObservable(); + Assert.IsNotNull(workflow); + } + + [TestMethod] + public void BuildObservable_ConvertibleWorkflowType_IsCompatibleAssignment() + { + var workflow = new TestWorkflow() + .AppendValue(1) + .AppendOutput() + .BuildObservable(); + Assert.IsNotNull(workflow); + } + class MergeBranchVisitor : ExpressionVisitor { public int BranchCount { get; private set; } diff --git a/Bonsai.Core.Tests/TestWorkflow.cs b/Bonsai.Core.Tests/TestWorkflow.cs index e973c7ed..5e08a549 100644 --- a/Bonsai.Core.Tests/TestWorkflow.cs +++ b/Bonsai.Core.Tests/TestWorkflow.cs @@ -108,9 +108,7 @@ public ExpressionBuilderGraph ToInspectableGraph() public IObservable BuildObservable() { - var expression = Workflow.Build(); - var observableFactory = Expression.Lambda>>(expression).Compile(); - return observableFactory(); + return Workflow.BuildObservable(); } } } diff --git a/Bonsai.Core/Expressions/ExpressionBuilder.cs b/Bonsai.Core/Expressions/ExpressionBuilder.cs index 0227d9c1..fcb794fe 100644 --- a/Bonsai.Core/Expressions/ExpressionBuilder.cs +++ b/Bonsai.Core/Expressions/ExpressionBuilder.cs @@ -474,7 +474,7 @@ static Expression[] ExpandParamArguments(ParameterInfo[] parameters, Expression[ var argument = arguments[k + offset]; if (argument.Type != arrayType) { - argument = CoerceMethodArgument(arrayType, argument); + argument = ConvertExpression(argument, arrayType); } initializers[k] = argument; } @@ -489,22 +489,23 @@ static Expression[] ExpandParamArguments(ParameterInfo[] parameters, Expression[ return expandedArguments; } - internal static Expression CoerceMethodArgument(Type parameterType, Expression argument) + internal static Expression ConvertExpression(Expression expression, Type parameterType) { - if (argument.Type.IsGenericType && parameterType.IsGenericType && - argument.Type.GetGenericTypeDefinition() == typeof(IObservable<>) && - parameterType.GetGenericTypeDefinition() == typeof(IObservable<>)) + if (expression.Type.IsGenericType && parameterType.IsGenericType && + expression.Type.GetGenericTypeDefinition() == typeof(IObservable<>) && + parameterType.GetGenericTypeDefinition() == typeof(IObservable<>) && + !parameterType.IsAssignableFrom(expression.Type)) { - var argumentObservableType = argument.Type.GetGenericArguments()[0]; + var argumentObservableType = expression.Type.GetGenericArguments()[0]; var parameterObservableType = parameterType.GetGenericArguments()[0]; var conversionParameter = Expression.Parameter(argumentObservableType); var conversion = Expression.Convert(conversionParameter, parameterObservableType); var select = selectMethod.MakeGenericMethod(argumentObservableType, parameterObservableType); - return Expression.Call(select, argument, Expression.Lambda(conversion, conversionParameter)); + return Expression.Call(select, expression, Expression.Lambda(conversion, conversionParameter)); } else { - return Expression.Convert(argument, parameterType); + return Expression.Convert(expression, parameterType); } } @@ -518,7 +519,7 @@ static Expression[] MatchMethodParameters(ParameterInfo[] parameters, Expression if (argument.Type != parameterType) { isCoerced |= !parameterType.IsAssignableFrom(argument.Type); - argument = CoerceMethodArgument(parameterType, argument); + argument = ConvertExpression(argument, parameterType); } return argument; }); diff --git a/Bonsai.Core/Expressions/ExpressionBuilderGraphExtensions.cs b/Bonsai.Core/Expressions/ExpressionBuilderGraphExtensions.cs index dd6257ce..4123e96c 100644 --- a/Bonsai.Core/Expressions/ExpressionBuilderGraphExtensions.cs +++ b/Bonsai.Core/Expressions/ExpressionBuilderGraphExtensions.cs @@ -1023,6 +1023,31 @@ public static IObservable BuildObservable(this ExpressionBuilderGraph sour return observableFactory(); } + /// + /// Builds and compiles an expression builder workflow into an observable sequence + /// with the specified element type. + /// + /// The type of the elements in the observable sequence. + /// The expression builder workflow to compile. + /// + /// An observable sequence with the specified element type. + /// + /// + /// The specified expression builder workflow does not compile into an observable sequence + /// with the expected element type. + /// + public static IObservable BuildObservable(this ExpressionBuilderGraph source) + { + var workflow = source.Build(); + if (!typeof(IObservable).IsAssignableFrom(workflow.Type)) + { + workflow = ExpressionBuilder.ConvertExpression(workflow, typeof(IObservable)); + } + + var observableFactory = Expression.Lambda>>(workflow).Compile(); + return observableFactory(); + } + static WorkflowExpressionBuilder Clone(this WorkflowExpressionBuilder builder, ExpressionBuilderGraph workflow) { var workflowExpression = (WorkflowExpressionBuilder)Activator.CreateInstance(builder.GetType(), workflow); diff --git a/Bonsai.Core/Expressions/ExternalizedDateTimeOffset.cs b/Bonsai.Core/Expressions/ExternalizedDateTimeOffset.cs index 25b97c13..b749c038 100644 --- a/Bonsai.Core/Expressions/ExternalizedDateTimeOffset.cs +++ b/Bonsai.Core/Expressions/ExternalizedDateTimeOffset.cs @@ -60,7 +60,7 @@ public override Expression Build(IEnumerable arguments) var propertySourceType = typeof(IObservable); if (source.Type != propertySourceType) { - source = CoerceMethodArgument(propertySourceType, source); + source = ConvertExpression(source, propertySourceType); } return source; diff --git a/Bonsai.Core/Expressions/ExternalizedProperty.cs b/Bonsai.Core/Expressions/ExternalizedProperty.cs index 3b75fba8..9857d86f 100644 --- a/Bonsai.Core/Expressions/ExternalizedProperty.cs +++ b/Bonsai.Core/Expressions/ExternalizedProperty.cs @@ -176,7 +176,7 @@ public override Expression Build(IEnumerable arguments) var propertySourceType = typeof(IObservable); if (source.Type != propertySourceType) { - source = CoerceMethodArgument(propertySourceType, source); + source = ConvertExpression(source, propertySourceType); } return source; diff --git a/Bonsai.Core/Expressions/ExternalizedTimeSpan.cs b/Bonsai.Core/Expressions/ExternalizedTimeSpan.cs index ac9feb0b..0c3c0ed4 100644 --- a/Bonsai.Core/Expressions/ExternalizedTimeSpan.cs +++ b/Bonsai.Core/Expressions/ExternalizedTimeSpan.cs @@ -60,7 +60,7 @@ public override Expression Build(IEnumerable arguments) var propertySourceType = typeof(IObservable); if (source.Type != propertySourceType) { - source = CoerceMethodArgument(propertySourceType, source); + source = ConvertExpression(source, propertySourceType); } return source;