Skip to content

Replace Three or More out ref Parameter Method

Akira Sugiura edited this page Oct 12, 2015 · 5 revisions

As you know, out/ref parameters should avoid to use because there are some warnings to note as below: CA1021: Avoid out parameters, CA1045: Do not pass types by reference and so on. Nevertheless, legacy code might not allow such rules. Prig supports a certain amount of the signature pattern same as Microsoft Fakes; otherwise you have to prepare it. We make up how to do that into this page.

Now, let's think about the code that calls Windows API by P/Invoke;

public class Foo
{
    [DllImport("kernel32.dll")]
    static extern IntPtr GetCurrentThread();

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetThreadTimes(IntPtr hThread, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime);

    public string FormatCurrentThreadTimes()
    {
        var creationTime = default(long);
        var exitTime = default(long);
        var kernelTime = default(long);
        var userTime = default(long);
        if (!GetThreadTimes(GetCurrentThread(), out creationTime, out exitTime, out kernelTime, out userTime))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        return string.Format("Creation Time: {0}, Exit Time: {1}, Kernel Time: {2}, User Time: {3}", creationTime, exitTime, kernelTime, userTime);
    }
}

FormatCurrentThreadTimes method will return the formatted string from the output of GetThreadTimes. Well, I think that test code will be unneeded in such small code 😅 -- Of course, I hope that there are no people that have a trauma against the code constellated with a lot of P/Invoke.

OK. At first, add a Stub Settings File same as usual:

Right click the Stub Settings File, start Prig Setup Session. And, execute the following commands to get the indirection setting for the target method:

PS> $TargetReferencedAssembly.GetTypes() | ? { $_.name -eq 'foo' } | pfind -m 'getthreadtimes' | pget | clip
PS> exit

Paste the result to the Stub Settings File, and build solution. Possibly, you think "OK, I can write test code with this!", start writing PFoo.GetThreadTimesIntPtrInt64RefInt64RefInt64RefInt64Ref().Body = ... without delay. Unfortunately, you can't build it. It is out of default pattern that is provided by Prig because GetThreadTimes has three or more parameters in this case. You have to require a little labor against such method.

Create new class library project(e.g. MyDelegates), and add Urasandesu.Prig.Framework as referenced assembly(This assembly is in C:\ProgramData\chocolatey\lib\Prig\lib).

Then, define the following delegate that has same signature as target method:

[IndirectionDelegate]
public delegate TResult IndirectionInOutOutOutOutFunc<in T1, TOut1, TOut2, TOut3, TOut4, TResult>(T1 arg1, out TOut1 out1, out TOut2 out2, out TOut3 out3, out TOut4 out4);

The point is that you have to apply the custom attribute and you had better give its name to be less likely to another. After building the project, execute the following command in the Package Manager Console:

PM> prig update All -delegate "$((dir .\MyDelegates\bin\Debug\MyDelegates.dll).FullName)"

This command means to extend the kind of the delegates that is supported by Prig. Add the delegates assembly as referenced assembly and rebuild solution.

Now, we have completed preparation! Let's write test code:

[Test]
public void FormatCurrentThreadTimes_on_execute_should_return_expected()
{
    using (new IndirectionsContext())
    {
        // Arrange
        PFoo.GetThreadTimesIntPtrInt64RefInt64RefInt64RefInt64Ref().Body = 
            (IntPtr hThread, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime) =>
            {
                lpCreationTime = 0;
                lpExitTime = 1;
                lpKernelTime = 2;
                lpUserTime = 3;
                return true;
            };

        
        // Act
        var actual = new Foo().FormatCurrentThreadTimes();

        
        // Assert
        Assert.AreEqual("Creation Time: 0, Exit Time: 1, Kernel Time: 2, User Time: 3", actual);
    }
}

It seems OK! 👍
For your information, the assembly that is installed by prig update command will be referenced through symbolic link. So, you don't have to re-execute the command each time unless the delegate saved path is changed.

Let me introduce another sample. This code indicates the chat room that the specified bots just have meaningless chat together:

public class ChatRoom
{
    public string Log { get; private set; }

    public void Start(ULTypeOfFeedback input, ULChatBot[] bots)
    {
        var reply = default(string);
        var action = default(ULActions);
        var recommendation = input;
        var result = default(bool);

        var sb = new StringBuilder(Log);

        foreach (var bot in bots)
        {
            result = bot.ReplyInformation(recommendation, out reply, ref action, out recommendation);
            sb.AppendFormat("Reply: {0} Action: {1} return? {2}\r\n", reply, action, result);
        }

        Log = sb.ToString();
    }
}

The class expressing ULChatBot is provided from other department -- as the library that have untestable interface 😵 :

public enum ULActions
{
    Unknown,
    Discard,
    ForwardToManagement,
    ForwardToDeveloper
}

public enum ULTypeOfFeedback
{
    Complaint,
    Praise,
    Suggestion,
    Incomprehensible
}

public class ULChatBot
{
    public ULTypeOfFeedback Recommendation { get; private set; }

    public bool ReplyInformation(ULTypeOfFeedback input, out string reply, ref ULActions action, out ULTypeOfFeedback recommendation)
    {
    	// ...
    }
}

I think that there is some case requiring confirmation, but in this case, we desided to confirm "Start should continue bots to chat using the recommendation of previous bot as the input of next bot". As same as the above example, add the Stub Settings File, get the Indirection Stub Setting in Prig Setup Session and paste them, then build solution. The delegate that has same signature as target method is the below:

[IndirectionDelegate]
public delegate TResult IndirectionInInOutRefOutFunc<in T1, in T2, TOut1, TRef1, TOut2, TResult>(T1 arg1, T2 arg2, out TOut1 out1, ref TRef1 ref1, out TOut2 out2);

Remember that this(in structure case, ref this) is passed as the first parameter if target method is an instance member. With this, you can write the following test code:

[Test]
public void Start_should_continue_bots_to_chat_using_the_recommendation_of_previous_bot_as_the_input_of_next_bot()
{
    using (new IndirectionsContext())
    {
        // Arrange
        var bots = new List<ULChatBot>();
        var actualInputs = new List<ULTypeOfFeedback>();

        var proxy1 = new PProxyULChatBot();
        proxy1.ReplyInformationULTypeOfFeedbackStringRefULActionsRefULTypeOfFeedbackRef().Body = 
            (ULChatBot @this, ULTypeOfFeedback input, out string reply, ref ULActions action, out ULTypeOfFeedback recommendation) => 
            {
                actualInputs.Add(input);
                reply = "1";
                recommendation = ULTypeOfFeedback.Suggestion;
                return true;
            };
        bots.Add(proxy1);

        var proxy2 = new PProxyULChatBot();
        proxy2.ReplyInformationULTypeOfFeedbackStringRefULActionsRefULTypeOfFeedbackRef().Body = 
            (ULChatBot @this, ULTypeOfFeedback input, out string reply, ref ULActions action, out ULTypeOfFeedback recommendation) =>
            {
                actualInputs.Add(input);
                reply = "2";
                recommendation = ULTypeOfFeedback.Incomprehensible;
                return true;
            };
        bots.Add(proxy2);


        // Act
        new ChatRoom().Start(ULTypeOfFeedback.Praise, bots.ToArray());


        // Assert
        var expectedInputs = new[] { ULTypeOfFeedback.Praise, ULTypeOfFeedback.Suggestion };
        CollectionAssert.AreEqual(expectedInputs, actualInputs);
    }
}

By the way, the person who has seen the auto generated Prig Type code might think that "I can directly use the infrastructure classes that are in these code against the above, can I? For example, Stub<OfPrigType>, Proxy<OfPrigProxyType> and so on.". Naturally, you can do it for practical purposes, but you had better stop it. Because any errors as the unintended signature changes will never be detected at the build time.

Complete source code is here.