Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#12: Add support for shimming async methods #29

Open
wants to merge 45 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b8aff4e
#26 Change `thisType.IsValueType` to `constructor.IsValueType`
Miista May 2, 2024
211a5f5
#26 Add regression test for Miista/pose#26
Miista May 2, 2024
e388857
Update references to “Pose” with “Poser”
Miista May 2, 2024
afe5f08
Update Poser.nuspec
Miista May 2, 2024
6a440d0
Merge pull request #40 from Miista/26-enumisdefined-cannot-be-called-…
Miista May 2, 2024
eea75ec
#12 Add support for async methods
Miista May 2, 2024
bd96fa7
Fix last reference to “Pose”
Miista May 2, 2024
228cc2a
#36 Update badges in README
Miista May 2, 2024
570af2b
Merge pull request #41 from Miista/release/release-2.0.1
Miista May 2, 2024
da84ffd
#12 Add tests for shimming async methods
Miista May 2, 2024
f4904c7
Merge pull request #32 from Miista/28-update-readme-to-use-poser-nuge…
Miista May 2, 2024
d4ed536
Merge pull request #42 from Miista/36-update-badges-in-readme
Miista May 2, 2024
5a47ab3
#12 Successfully add support for async methods
Miista May 2, 2024
447b3ed
#12 Add net7.0 target to tests
Miista May 2, 2024
3ff8edb
#12 Clean up implementation
Miista May 2, 2024
b10d8b5
Merge remote-tracking branch 'origin/master' into 12-add-async-support
Miista May 2, 2024
9ed298f
#12 Bump version to 2.1.0-alpha0001
Miista May 2, 2024
95b9f35
#12: Add tests for async stubbing and getting the MoveNext method
Miista May 2, 2024
de427ae
#12 Add tests for more coverage
Miista May 2, 2024
599f7fc
Add more tests for coverage
Miista May 2, 2024
170a919
Remove redundant platform override #if
Miista May 2, 2024
1373e9d
Remove netcoreapp2.1 from test targets
Miista May 2, 2024
52e3c89
Remove netstandard2.1 from test target frameworks
Miista May 2, 2024
8db27e6
#12 Add examples to README
Miista May 2, 2024
3663b77
Swap usages of DEBUG with TRACE
Miista May 2, 2024
afcce01
#12 Begin adding tests for replacing async methods
Miista May 2, 2024
da34def
#12 Rearrange sections in README for clarity
Miista May 2, 2024
44e57ef
Merge remote-tracking branch 'origin/master' into 12-add-async-support
Miista May 2, 2024
cdf8430
#12 Emit leave instruction if rewriting an async method
Miista May 2, 2024
bef32fb
Update references to “Pose” with “Poser”
Miista Jan 16, 2025
a3df3d4
Fix last reference to “Pose”
Miista Jan 16, 2025
0e9881f
Merge pull request #32 from Miista/28-update-readme-to-use-poser-nuge…
Miista Jan 16, 2025
ddfb550
#26 Change `thisType.IsValueType` to `constructor.IsValueType`
Miista Jan 16, 2025
790b2ae
#26 Add regression test for Miista/pose#26
Miista Jan 16, 2025
d1a5a13
Update Poser.nuspec
Miista Jan 16, 2025
16abe83
Merge pull request #40 from Miista/26-enumisdefined-cannot-be-called-…
Miista Jan 16, 2025
297b8f1
#36 Update badges in README
Miista Jan 16, 2025
189685f
Merge pull request #41 from Miista/release/release-2.0.1
Miista Jan 16, 2025
9289b33
Merge pull request #42 from Miista/36-update-badges-in-readme
Miista Jan 16, 2025
e29b229
Merge remote-tracking branch 'refs/remotes/origin/master' into 12-add…
Miista Jan 16, 2025
0d2c6d7
#12: Add special case for rewriting AsyncMethodBuilderCore
Miista Jan 16, 2025
5874cc9
Something which works
Miista Jan 16, 2025
274d518
More stuff that nearly works
Miista Jan 16, 2025
697b39d
Successfully rewrite async method
Miista Jan 16, 2025
5b5908e
Add more code
Miista Jan 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[![Build status](https://dev.azure.com/palmund/Pose/_apis/build/status/Pose-CI?branchName=master)](https://dev.azure.com/palmund/Pose/_build/latest?definitionId=12)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![NuGet version](https://badge.fury.io/nu/Poser.svg)](https://www.nuget.org/packages/Poser)
# Pose
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Build status](https://dev.azure.com/palmund/Pose/_apis/build/status/Pose-CI?branchName=master&Label=build)](https://dev.azure.com/palmund/Pose/_build/latest?definitionId=12)
[![NuGet version](https://img.shields.io/nuget/v/Poser?logo=nuget)](https://www.nuget.org/packages/Poser)
[![NuGet preview version](https://img.shields.io/nuget/vpre/Poser?logo=nuget)](https://www.nuget.org/packages/Poser)

Pose allows you to replace any .NET method (including static and non-virtual) with a delegate. It is similar to [Microsoft Fakes](https://msdn.microsoft.com/en-us/library/hh549175.aspx) but unlike it Pose is implemented _entirely_ in managed code (Reflection Emit API). Everything occurs at runtime and in-memory, no unmanaged Profiling APIs and no file system pollution with re-written assemblies.
# Poser

Pose is cross platform and runs anywhere .NET is supported. It targets .NET Standard 2.0 so it can be used across .NET platforms including .NET Framework, .NET Core, Mono and Xamarin. See version compatibility table [here](https://docs.microsoft.com/en-us/dotnet/standard/net-standard).
Poser allows you to replace any .NET method (including static and non-virtual) with a delegate. It is similar to [Microsoft Fakes](https://msdn.microsoft.com/en-us/library/hh549175.aspx) but unlike it Poser is implemented _entirely_ in managed code (Reflection Emit API). Everything occurs at runtime and in-memory, no unmanaged Profiling APIs and no file system pollution with re-written assemblies.

Poser is cross platform and runs anywhere .NET is supported. It targets .NET Standard 2.0 so it can be used across .NET platforms including .NET Framework, .NET Core, Mono and Xamarin. See version compatibility table [here](https://docs.microsoft.com/en-us/dotnet/standard/net-standard).

## Installation

Expand All @@ -14,18 +16,18 @@ Available on [NuGet](https://www.nuget.org/packages/Poser/)
Visual Studio:

```powershell
PM> Install-Package Pose
PM> Install-Package Poser
```

.NET Core CLI:

```bash
dotnet add package Pose
dotnet add package Poser
```

## Usage

Pose gives you the ability to create shims by way of the `Shim` class. Shims are basically objects that let you specify the method you want to replace as well as the replacement delegate. Delegate signatures (arguments and return type) must match that of the methods they replace. The `Is` class is used to create instances of a type and all code you want to apply your shims to is isolated using the `PoseContext` class.
Poser gives you the ability to create shims by way of the `Shim` class. Shims are basically objects that let you specify the method you want to replace as well as the replacement delegate. Delegate signatures (arguments and return type) must match that of the methods they replace. The `Is` class is used to create instances of a type and all code you want to apply your shims to is isolated using the `PoseContext` class.


### Shim static method
Expand Down Expand Up @@ -139,14 +141,74 @@ PoseContext.Isolate(() =>
}, consoleShim, dateTimeShim, classPropShim, classShim, myClassShim, structShim);
```

## Async usage
### Shim static async method
```csharp
using Pose;

Shim staticTaskShim = Shim.Replace(() => DoWorkAsync()).With(
delegate
{
Console.Write("refusing to do work");
return Task.CompletedTask;
});
```

### Shim async instance method of a Reference Type
```csharp
using Pose;

Shim instanceTaskShim = Shim.Replace(() => Is.A<MyClass>().DoSomethingAsync()).With(
delegate(MyClass @this)
{
Console.WriteLine("doing something else async");
return Task.CompletedTask;
});
```

### Shim method of specific instance of a Reference Type
_Not supported for now. When supported, however, it will look like the following._

```csharp
using Pose;

MyClass myClass = new MyClass();
Shim myClassTaskShim = Shim.Replace(() => myClass.DoSomethingAsync()).With(
delegate(MyClass @this)
{
Console.WriteLine("doing something else with myClass async");
return Task.CompletedTask;
});
```

### Isolating your async code

```csharp
// This block executes immediately
await PoseContext.Isolate(async () =>
{
// All code that executes within this block
// is isolated and shimmed methods are replaced

// Outputs "refusing to do work"
await DoWorkAsync();

// Outputs "doing something else async"
new MyClass().DoSomethingAsync();

// Outputs "doing something else with myClass async"
await myClass.DoSomethingAsync();

}, staticTaskShim, instanceTaskShim, myClassTaskShim);
```
## Caveats & Limitations

* **Breakpoints** - At this time any breakpoints set anywhere in the isolated code and its execution path will not be hit. However, breakpoints set within a shim replacement delegate are hit.
* **Exceptions** - At this time all unhandled exceptions thrown in isolated code and its execution path are always wrapped in `System.Reflection.TargetInvocationException`.

## Roadmap

* **Performance Improvements** - Pose can be used outside the context of unit tests. Better performance would make it suitable for use in production code, possibly to override legacy functionality.
* **Performance Improvements** - Poser can be used outside the context of unit tests. Better performance would make it suitable for use in production code, possibly to override legacy functionality.
* **Exceptions Stack Trace** - Currently when exceptions are thrown in your own code under isolation, the supplied exception stack trace is quite confusing. Providing an undiluted exception stack trace is needed.

## Issues & Contributions
Expand Down
4 changes: 2 additions & 2 deletions nuget/Poser.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Poser</id>
<version>2.0.0</version>
<version>2.0.1</version>
<title>Pose</title>
<authors>Søren Guldmund</authors>
<owners>Søren Guldmund</owners>
Expand All @@ -16,7 +16,7 @@
<copyright>Copyright 2024</copyright>
<readme>docs\README.md</readme>
<releaseNotes>
Provide better exception message when we cannot create instance.
Fix bug where `Enum.IsDefined` could not be called from within `PoseContext.Isolate`.
</releaseNotes>

<dependencies>
Expand Down
64 changes: 64 additions & 0 deletions src/Pose/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Pose.Extensions
{
internal static class TypeExtensions
{
public static bool ImplementsInterface<TInterface>(this Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (!typeof(TInterface).IsInterface) throw new InvalidOperationException($"{typeof(TInterface)} is not an interface.");

return type.GetInterfaces().Any(interfaceType => interfaceType == typeof(TInterface));
}

public static bool HasAttribute<TAttribute>(this Type type) where TAttribute : Attribute
{
if (type == null) throw new ArgumentNullException(nameof(type));

var compilerGeneratedAttribute = type.GetCustomAttribute<TAttribute>() ?? type.ReflectedType?.GetCustomAttribute<TAttribute>();

return compilerGeneratedAttribute != null;
}

public static MethodInfo GetExplicitlyImplementedMethod<TInterface>(this Type type, string methodName)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(methodName));

var interfaceType = type.GetInterfaceType<TInterface>() ?? throw new Exception();
var method = interfaceType.GetMethod(methodName) ?? throw new Exception();
var methodDeclaringType = method.DeclaringType ?? throw new Exception($"The {methodName} method does not have a declaring type");
var interfaceMapping = type.GetInterfaceMap(methodDeclaringType);
var requestedTargetMethod = interfaceMapping.TargetMethods.FirstOrDefault(m => m.Name == methodName);

return requestedTargetMethod;
}

private static Type GetInterfaceType<TInterface>(this Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (!typeof(TInterface).IsInterface) throw new InvalidOperationException($"{typeof(TInterface)} is not an interface.");

return type.GetInterfaces().FirstOrDefault(interfaceType => interfaceType == typeof(TInterface));
}

public static bool IsAsync(this Type thisType)
{
if (thisType == null) throw new ArgumentNullException(nameof(thisType));

return
// State machines are generated by the compiler...
thisType.HasAttribute<CompilerGeneratedAttribute>()

// as nested private classes...
&& thisType.IsNestedPrivate

// which implements IAsyncStateMachine.
&& thisType.ImplementsInterface<IAsyncStateMachine>();
}
}
}
11 changes: 6 additions & 5 deletions src/Pose/Helpers/StubHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

using System.Runtime.CompilerServices;
using Pose.Extensions;

namespace Pose.Helpers
Expand Down Expand Up @@ -58,6 +58,11 @@ public static MethodInfo DeVirtualizeMethod(Type thisType, MethodInfo virtualMet

var bindingFlags = BindingFlags.Instance | (virtualMethod.IsPublic ? BindingFlags.Public : BindingFlags.NonPublic);
var types = virtualMethod.GetParameters().Select(p => p.ParameterType).ToArray();

if (thisType.IsAsync())
{
return thisType.GetExplicitlyImplementedMethod<IAsyncStateMachine>(nameof(IAsyncStateMachine.MoveNext));
}

return thisType.GetMethod(virtualMethod.Name, bindingFlags, null, types, null);
}
Expand Down Expand Up @@ -94,11 +99,7 @@ public static string CreateStubNameFromMethod(string prefix, MethodBase method)
if (genericArguments.Length > 0)
{
name += "[";
#if NETSTANDARD2_1_OR_GREATER
name += string.Join(',', genericArguments.Select(g => g.Name));
#else
name += string.Join(",", genericArguments.Select(g => g.Name));
#endif
name += "]";
}
}
Expand Down
Loading