Why does the generic type parameter of a Variable<T> have to exactly match a corresponding OutArgument<T> that sets it?

StackOverflow https://stackoverflow.com/questions/23444826

  •  14-07-2023
  •  | 
  •  

Question

Consider the following C# code:

public static Foo CreateFoo()
{
    return new Foo();
}

// ...

public static void Main()
{
    IFoo foo = CreateFoo();
}

Fairly straightforward: assignment statements allow the target variable to be a less specific type than the expression's type.

So I expected the commented-out line to work, but it fails for the reason identified below, forcing anything that sets TheFoo to return an instance of the concrete Foo class instead of just an IFoo:

Child Activity Definition

public class ActivityWithOutArgument : CodeActivity<Foo>
{
    protected override Foo Execute(CodeActivityContext context)
    {
        return new Foo();
    }
}

Driver

<Activity x:Class="MyNamespace.TheActivity"
          xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:mine="clr-namespace:MyNamespace">
  <Sequence>
    <Sequence.Variables>
      <!--
      The following fails on the "mine:ActivityWithOutArgument" line, with:
      Compiler error(s) encountered processing expression "TheFoo".
      Option Strict On disallows implicit conversions from 'MyNamespace.IFoo' to 'MyNamespace.Foo'.

      <Variable x:TypeArguments="mine:IFoo" Name="TheFoo" />
      -->

      <!-- This works -->
      <Variable x:TypeArguments="mine:Foo" Name="TheFoo" />
    </Sequence.Variables>

    <mine:ActivityWithOutArgument Result="[TheFoo]" />
  </Sequence>
</Activity>

Context (long-winded, sorry)

This came up when I was dealing with legacy code that returns a particularly nasty concrete class, which is fairly obnoxious to work with in our test suite (for example, all tests that create instances of this bugger fail on our build server but pass locally, and fixing that issue is non-trivial).

We have a few existing activities with OutArgument<Foo> already (and all of the tests for them fail on the build server, and call into a really annoying static method). So when I was tasked with creating a counterpart activity to use internally, I thought "OK, so let's have the new activity use OutArgument<IFoo> so that I can test it using a mock".

So the logic inside of the calling activity would look something like (using C# as an analogy):

public static void Run()
{
    IFoo foo = TryExistingActivity();
    if (foo == null)
    {
        foo = UseMyShinyNewActivity();
    }
}

I guess there's a workaround to make it look something like this:

public static void Run()
{
    IFoo foo;

    Foo uglyWorkaround = TryExistingActivity();
    if (uglyWorkaround == null)
    {
        foo = UseMyShinyNewActivity();
    }
    else
    {
        foo = uglyWorkaround;
    }
}

...and this may be what I end up doing, though I'm inclined to just conform to the legacy stuff and use shims, the most cheating thing I've ever seen, as a magic wand to make the unit tests not fail.

Summary

Fundamentally, why can't I use a Variable<IFoo> to store the result of an activity's OutArgument<Foo>? Or is this just the standard "implementation cost vs benefit" thing?

Edit 1: Reproduced entirely in C#

This uses the same activity from "Child Activity Definition" above.

class Program
{
    static void Main()
    {
        Variable<IFoo> theFoo = new Variable<IFoo>("TheFoo");
        Sequence sequence = new Sequence
                            {
                                Variables =
                                {
                                    theFoo
                                },
                                Activities =
                                {
                                    new ActivityWithOutArgument
                                    {
                                        Result = theFoo
                                    }
                                }
                            };

        // The following fails with:
        // InvalidWorkflowException was Unhandled
        // The following errors were encountered while processing the workflow tree:
        // 'VariableReference<Foo>': Variable 'System.Activities.Variable`1[MyNamespace.IFoo]' cannot be used
        // in an expression of type 'MyNamespace.Foo', since it is of type 'MyNamespace.IFoo' which is not compatible.
        WorkflowInvoker.Invoke(sequence);
    }
}
Was it helpful?

Solution

Because the way WF is designed, inputs and outputs are represented by generic classes, which cannot be covariant or contravariant.

Nothing fundamental to WF prevents this, however, so this works:

using System;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Expressions;

namespace WorkflowConsoleApplication1
{
    public interface IFoo
    {
        void DoThing();
    }

    public sealed class Foo : IFoo
    {
        public void DoThing() => Console.WriteLine("Hello");
    }

    public sealed class ActivityWithOutArgument : CodeActivity<Foo>
    {
        protected override Foo Execute(CodeActivityContext context) => new Foo();
    }

    internal static class Program
    {
        private static void Main()
        {
            Variable<IFoo> theFoo = new Variable<IFoo>(nameof(theFoo));
            Sequence sequence = new Sequence
            {
                Variables =
                {
                    theFoo,
                },
                Activities =
                {
                    new Assign<IFoo>
                    {
                        To = theFoo,
                        Value = new Cast<Foo, IFoo>
                        {
                            Operand = new ActivityWithOutArgument(),
                        },
                    },
                    new InvokeMethod
                    {
                        TargetObject = new InArgument<IFoo>(theFoo),
                        MethodName = nameof(IFoo.DoThing),
                    },
                }
            };

            WorkflowInvoker.Invoke(sequence);
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top