GithubHelp home page GithubHelp logo

devlooped / avatar Goto Github PK

View Code? Open in Web Editor NEW
139.0 4.0 10.0 1.35 MB

A modern compile-time generated interception/proxy library

Home Page: https://clarius.org/avatar

License: MIT License

C# 99.06% Visual Basic .NET 0.73% Ruby 0.01% Dockerfile 0.17% SCSS 0.03%
proxy-generation dynamicproxy csharp-sourcegenerator

avatar's Introduction

Icon Avatar

Avatar is a modern interception library which implements the proxy pattern and runs everywhere, even where run-time code generation (Reflection.Emit) is forbidden or limitted, like physical iOS devices and game consoles, through compile-time code generation. The proxy behavior is configured in code using what we call a behavior pipeline.

Avatars blend in with the Na'vi seamlessly, and you can control their behavior precisely by 'driving' them through a psionic link. Just like a proxy, with behavior driven through code.

Avatar Overloads

Version Downloads License Discord Chat GitHub

CI Version GH CI Status

NOTE: Avatar provides a fairly low-level API with just the essential building blocks on top of which higher-level APIs can be built, such as the upcoming Moq vNext API.

Requirements

Avatar is a .NET Standard 2.0 library and runs on any runtime that supports that.

Compile-time proxy generation leverages Roslyn source generators and therefore requires C# 9.0, which at this time is included in Visual Studio 16.8 (preview or later) and the .NET 5.0 SDK (RC or later). Compile-time generated proxies support the broadest possible run-time platforms since they don't require any Reflection.Emit, and also don't pay that performance cost either.

Whenever compile-time proxy generation is not available, a fallback generation strategy is used instead, which leverages Castle DynamicProxy to provide the run-time code generation.

The client API for configuring proxy behaviors in either case is exactly the same.

NOTE: even though generated proxies is the main usage for Avatar, the API was designed so that you can also consume the behavior pipeline easily from hand-coded proxies too.

Usage

ICalculator calc = Avatar.Of<ICalculator>();

calc.AddBehavior((invocation, next) => ...);

AddBehavior/InsertBehavior overloads allow granular control of the avatar's behavior pipeline, which is basically a chain of responsibility that invokes all configured behaviors that apply to the current invocation. Individual behaviors can determine whether to short-circuit the call or call the next behavior in the chain.

Avatar Overloads

Behaviors can also dynamically determine whether they apply to a given invocation by providing the optional appliesTo argument. In addition to the delegate-based overloads (called anonymous behaviors), you can also create behaviors by implementing the IAvatarBehavior interface:

public interface IAvatarBehavior
{
    bool AppliesTo(IMethodInvocation invocation);
    IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next);
}

Common Behaviors

Some commonly used behaviors that are generally useful are provided in the library and can be added to avatars as needed:

  • DefaultValueBehavior: sets default values for method return and out arguments. In addition to the built-in supported default values, additional default value factories can be registered for any type.

  • DefaultEqualityBehavior: implements the Object.Equals and Object.GetHashCode members just like System.Object implements them.

  • RecordingBehavior: simple behavior that keeps track of all invocations, for troubleshooting or reporting.

Customizing Avatar Creation

If you want to centrally configure all your avatars, the easiest way is to simply provide your own factory method (i.e. Stub.Of<T>), which in turn calls the Avatar.Of<T> provided. For example:

    public static class Stub
    {
        [AvatarGenerator]
        public static T Of<T>() => Avatar.Of<T>()
            .AddBehavior(new RecordingBehavior())
            .AddBehavior(new DefaultEqualityBehavior())
            .AddBehavior(new DefaultValueBehavior());
    }

The [AvatarGenerator] attribute is required if you want to leverage the built-in compile-time code generation, since that signals to the source generator that calls to your API end up creating an avatar at run-time and therefore a generated type will be needed for it during compile-time. You can actually explore how this very same behavior is implemented in the built-in Avatar API which is provided as a content file:

avatar API source

The Avatar.cs contains, for example:

[AvatarGenerator]
public static T Of<T>(params object[] constructorArgs) => Create<T>(constructorArgs);

[AvatarGenerator]
public static T Of<T, T1>(params object[] constructorArgs) => Create<T>(constructorArgs, typeof(T1));

As you can see, the Avatar API itself uses the same extensibility mechanism that your own custom factory methods can use.

Static vs Dynamic Avatars

Depending on the project and platform, Avatars will automatically choose whether to use run-time proxies or compile-time ones (powered by Roslyn source generators). The latter are only supported when building C# 9.0+ projects.

You can opt out of the static avatars by setting EnableCompiledAvatars=false in your project file:

<PropertyGroup>
    <EnableCompiledAvatars>false</EnableCompiledAvatars>
</PropertyGroup>

This will switch the project to run-time proxies based on Castle.Core.

Debugging Optimizations

There is nothing more frustrating than a proxy you have carefully configured that doesn't behave the way you expect it to. In order to make this a less frustrating experience, avatars are carefully optimized for debugger display and inspection, so that it's clear what behaviors are configured, and invocations and results are displayed clearly and concisely. Here's the debugging display of the RecordingBehavior that just keeps track of invocations and their return values for example:

debugging display

And here's the invocation debugger display from an anonymous behavior:

behavior debugging

Samples

The samples folder in the repository contains a few interesting examples of how Avatar can be used to implement some fancy use cases. For example:

  • Forwarding calls to matching interface methods/properties (by signature) to a static class. The example uses this to wrap calls to System.Console via an IConsole interface.

  • Forwarding calls to a target object using the DLR (that backs the dynamic keyword in C#) API for high-performance late binding.

  • Custom Stub.Of<T> factory that creates avatars that have common behaviors configured automatically.

  • Custom avatar factory method that adds an int return value randomizer.

  • Configuring the built-in DefaultValueBehavior so that every time a string property is retrieved, it gets a random lorem ipsum value.

  • Logging all calls to an avatar to the Xunit output helper.

Sponsors

sponsors  by @clarius sponsors

get mentioned here too!

avatar's People

Contributors

atifaziz avatar dependabot[bot] avatar kzu avatar stakx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

avatar's Issues

Rename callBase to implementation

In preparation for adding decorator support, it's becoming clearer that that callBase delegate is actually the implementation of a method, be it from a call to the base implementation in a virtual method or the invocation of a target object provided as the instance being decorated for an interface avatar.

In order to avoid having two distinct concepts for the same idea (i.e. in IMethodInvocation we have a CreateCallBaseReturn), rename things consistently so we only talk about the ultimate implementation of the method, so, HasImplementation and CreateInvokeReturn (to invoke that implementation).

When we introduce decorators, we will not need to add additional public API members in the pipeline for that.

Don't force usage of a specific compiler toolset

The quick and easy fix for #52 was to simply add a dependency on the one compiler toolset we compile our generator against. This isn't great as it prevents leveraging any new compiler features (i.e. during a VS preview). Moreover, this was completely opaque to the user, since it was just a dependency we pulled in, so they had no heads-up that we were changing the compiler for them.

A better approach is to instead support a range of compiler versions, while having an easy mechanism for adding more supported compiler versions down the road. Initial test should be (alongside an acceptance test) to install VS preview and ensure the acceptance tests still pass using the preview compiler included. Once the preview becomes stable, we can bump the version and be done.

A friendly error message should state clearly if a given compiler version is not supported and provide actionable suggestions on how to fix it, namely: install a compiler toolset that is compatible (suggest latest supported version) or turn off compile-time avatars entirely with EnableCompileTimeAvatars=false.

Remove dependency on code fixers and refactorings from Roslyn internals

Currently we leverage built-in (internals, but fairly easily accessible) internal
code fixers and refactorings to scaffold the basic type boilerplate, just as it
would be generated on the IDE by applying the provided code fix providers
(specifically the "Implement Abstract Class", "Implement Interface Members"
and "Generate Default Constructors").

This creates a tighter than necessary coupling with the specific versions of
Roslyn we support, forcing us to ship one version of the "tools" dependencies
for each supported Roslyn version (atm 3.8, 3.9 and 3.10). This adds unnecessary
bulk to the package size, but also adds non-trivial requirements for integration
testing and maintainability burden as future Roslyn versions come out (since
we need to support each major.minor specifically).

At this point it's becoming clear that it might be better to just reimplement
these built-in scaffold behaviors natively on top of plain C# syntax, since (for
the moment) we know C# is going to be the only supported generator for a
while. We also have the dynamic proxy fallback just in case. So this would be
a big win in terms of simplicity and maintainability.

The non-trivial part of the issue is that, "shockingly", most of the behavior that
drives the built-in code fixers/refactorings, relies on entirely internal behavior
we'll need to extract/copy and then keep up to date. But the trade-off is to
keep and ship multiple roslyn "toolsets", so it's not like the (current) alternative
has no cost.

A quick spike on default constructors generator was promising, so it might be
worth trying to replace all three built-in behaviors and remove all the internals
accesses.

As an added side-benefit, this should have a noticeable impact in performance
too, since we currently have to spin up a proper workspace and project for the
scaffolding to actually work at all.

Allow invoking base (virtual) method directly from IMethodInvocation

Based on feedback from @stakx, it looks like the way we currently implement CallBase in Moq is quite convoluted (it involves skipping other behaviors dynamically, tightly coupling the call base behavior to the other configured behaviors).

Instead of that, just add a new API to the IMethodInvocation to directly create a return by calling the base method. Proposal:

IMethodReturn CreateCallBaseReturn(IArgumentCollection? arguments = null);

The optional arguments would allow the caller/behavior to perform any last-minute tweaks to the arguments ultimately used in the base call prior to returning a value (and optional ref/out args).

This also means we can simplify the behavior pipeline API itself, since there would be no concept of a target to invoke, just the pipeline itself. The pipeline would need to be modified so that invoking it for a method with no implementation (abstract or interface) and with no configured behaviors, throws NotImplementedException now automatically.

Source generator fails when used in Visual Studio preview

Describe the Bug

If the compiler in use doesn't match the source generator dependencies, code generation fails.

Steps to Reproduce

Install VS preview, install Avatar nuget on a project. Compiler issues a warning like:

CSC : warning CS8785: Generator 'AvatarGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'TypeInitializationException' with message 'The type initializer for 'Avatars.WorkspaceServices' threw an exception.'

This later fails at run-time because the expected avatar types aren't present in the assembly.

Expected Behavior

Things should Just Work :)

Error when packing because of mismatch of System.Runtime.CompilerServices.Unsafe

The Avatar.CodeAnalysis project, via its reference Microsoft.CodeAnalysis.Common brings in transitive dependency to "System.Runtime.CompilerServices.Unsafe/4.7.1".

The Avatar project, however, via its reference System.Threading.Tasks.Extensions brings in a transitive dependency to "System.Runtime.CompilerServices.Unsafe/4.5.3".

This causes the packing to fail because two distinct versions of that assembly are targeted to the same folder when packing the first project for standalone referencing (i.e. for extending the analyzer process, like Moq does).

Error message reads:

Duplicate package source files with distinct content detected. Duplicates are not allowed in the package. Please remove the conflict between these files: 
'C:\Users\danie\.nuget\packages\system.runtime.compilerservices.unsafe\5.0.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll' > 'lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll'
'C:\Users\danie\.nuget\packages\system.runtime.compilerservices.unsafe\4.5.3\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll' > 'lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll'

Simplify behavior execution delegate signature

Chain of responsibility pattern implementation has a few variants, but it seems to be more common for the "next" delegate/handler to actually be a direct reference to the next handler in the chain, so you can execute it directly. This simplifies the calling pattern and signature since you change from: next().Invoke(...) to just next.Invoke(...) which is far more natural.

Moving the "getting next" (or throwing if not valid) to be part of the invocation of the delegate itself, simplifies the consumers of the pipeline at a just a little expense on the behavior pipeline side. This is a net plus IMO.

In addition, it's explicitly advised against to append Delegate to delegate names, so we should instead rename consistently to ***Handler instead (ExecuteHandler and AppliesToHandler). We'd have one less delegate since the GetNextBehavior would go away.

Provide simple property to opt out of static proxies

Dynamic proxies based on Castle.Core are super stable, well supported and have stood the test of time.

Static proxies based on source generators are brand-new and therefore lack the broad testing in the wild, so it should be very easy (and with a documented property) to turn them off if things don't quite work for particular scenarios.

Proposed property: EnableCompiledAvatars. Not mentioning "source generator" in the property since that's an implementation detail people may not easily recognize, and "compiled" conveys that these are things that happen at compile-time, rather than run-time like the dynamic proxies (which are better known).

The property should be true by default whenever the project supports source generators (C#9+).

Add support for nested types

Nested types used to be unsupported, but nowadays we can generate them just fine. Remove the analyzer that warns about nested types not being supported and add proper tests for this scenario.

Avatar source generation processors should get updated compilation from context

Currently, if a generator adds members to the syntax node being processed,
attempting to use the context compilation to retrieve symbol info would fail
since we're not updating the compilation after each generator run.

For completeness and consistency, we should do so, so that if any processor
happens to need compilation/symbol information, they can rely on the context
provided compilation being up-to-date with regards to code generated by
preceding processors.

Remove dependency on Workspace API in the public API surface for avatar generator pipeline

Currently we expose the IDocumentProcessor as the main extensiblity API to inject code into generated avatars. This limits our possibilities to remove the dependency on Roslyn code fixes and refactories we currently use for the initial scaffolding.

The proposed updated API should be:

    public interface IAvatarProcessor
    {
        string Language { get; }
        ProcessorPhase Phase { get; }
        SyntaxNode Process(SyntaxNode syntax, ProcessorContext context);
    }

We should keep Language in case other languages adopt source generators too.

Internally, we should also decouple all processors from any knowledge there's a document (and implicitly the workspace and SyntaxGenerator API) and rely instead on pure SyntaxFactory for codegen.

Nullable warnings when out/ref argument is nullable

Given the following method signature:

bool TryAdd(ref int x, ref int y, out int? z)

The generated code contains lines like:

var _z = m.Arguments.Get<int?>("z");
...
z = _result.Outputs.Get<int?>("z");

This causes a nullable constraint warning since the Get<T> extension method on IArgumentCollection specifies notnull constraint for T, unlike the GetNullable<T> method.

We should be detecting whether the type we're retrieving from the collection is nullable or not and invoke the right method according.

When emitting generated files, whitespace/newlines are missing

We're calling NormalizeWhitespace() on the syntax node after applying all processors, and while this ensures we add properly formatted source (that can be parsed anyway) and is mostly readable, some members miss proper newlines. Example:

Missing newlines
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable
#pragma warning disable CS8600, CS8601, CS8602, CS8603, CS8604, CS8605, CS8618, CS8625, CS8765
using Avatars;
using Sample;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Avatars.Sample
{
    [CompilerGenerated]
    partial class ICalculatorAvatar : ICalculator, IAvatar
    {
        readonly BehaviorPipeline pipeline = BehaviorPipelineFactory.Default.CreatePipeline<ICalculatorAvatar>();
        [CompilerGenerated]
        public ICalculatorAvatar() => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateReturn()));
        [CompilerGenerated]
        IList<IAvatarBehavior> IAvatar.Behaviors => pipeline.Behaviors;
        [CompilerGenerated]
        public int? this[string name]
        {
            get => pipeline.Execute<int?>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));
            set => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name, value));
        }

        [CompilerGenerated]
        public bool IsOn => pipeline.Execute<bool>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));
        [CompilerGenerated]
        public CalculatorMode Mode
        {
            get => pipeline.Execute<CalculatorMode>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));
            set => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
        }

        [CompilerGenerated]
        public ICalculatorMemory Memory => pipeline.Execute<ICalculatorMemory>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));
        [CompilerGenerated]
        public int Add(int x, int y) => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), x, y));
        [CompilerGenerated]
        public int Add(int x, int y, int z) => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), x, y, z));
        [CompilerGenerated]
        public void Clear(string name) => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));
        [CompilerGenerated]
        public override bool Equals(object obj) => pipeline.Execute<bool>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.Equals(obj)), obj));
        [CompilerGenerated]
        public override int GetHashCode() => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.GetHashCode())));
        [CompilerGenerated]
        public int? Recall(string name) => pipeline.Execute<int?>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));
        [CompilerGenerated]
        public void Store(string name, int value) => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name, value));
        [CompilerGenerated]
        public override string ToString() => pipeline.Execute<string>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.ToString())));
        [CompilerGenerated]
        public bool TryAdd(ref int x, ref int y, out int z)
        {
            var _method = MethodBase.GetCurrentMethod();
            z = default;
            var _result = pipeline.Invoke(MethodInvocation.Create(this, _method, x, y, z));
            x = _result.Outputs.Get<int>("x");
            y = _result.Outputs.Get<int>("y");
            z = _result.Outputs.Get<int>("z");
            return (bool)_result.ReturnValue!;
        }

        [CompilerGenerated]
        public void TurnOn() => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));
        [CompilerGenerated]
        public event EventHandler TurnedOn
        {
            add => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
            remove => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
        }
    }
}
Expected newlines
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable
#pragma warning disable CS8600, CS8601, CS8602, CS8603, CS8604, CS8605, CS8618, CS8625, CS8765
using Avatars;
using Sample;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Avatars.Sample
{
    [CompilerGenerated]
    partial class ICalculatorAvatar : ICalculator, IAvatar
    {
        readonly BehaviorPipeline pipeline = BehaviorPipelineFactory.Default.CreatePipeline<ICalculatorAvatar>();

        [CompilerGenerated]
        public ICalculatorAvatar() => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateReturn()));

        [CompilerGenerated]
        IList<IAvatarBehavior> IAvatar.Behaviors => pipeline.Behaviors;

        [CompilerGenerated]
        public int? this[string name]
        {
            get => pipeline.Execute<int?>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));
            set => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name, value));
        }

        [CompilerGenerated]
        public bool IsOn => pipeline.Execute<bool>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));

        [CompilerGenerated]
        public CalculatorMode Mode
        {
            get => pipeline.Execute<CalculatorMode>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));
            set => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
        }

        [CompilerGenerated]
        public ICalculatorMemory Memory => pipeline.Execute<ICalculatorMemory>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));

        [CompilerGenerated]
        public int Add(int x, int y) => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), x, y));

        [CompilerGenerated]
        public int Add(int x, int y, int z) => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), x, y, z));

        [CompilerGenerated]
        public void Clear(string name) => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));

        [CompilerGenerated]
        public override bool Equals(object obj) => pipeline.Execute<bool>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.Equals(obj)), obj));

        [CompilerGenerated]
        public override int GetHashCode() => pipeline.Execute<int>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.GetHashCode())));

        [CompilerGenerated]
        public int? Recall(string name) => pipeline.Execute<int?>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name));

        [CompilerGenerated]
        public void Store(string name, int value) => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), name, value));

        [CompilerGenerated]
        public override string ToString() => pipeline.Execute<string>(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), (m, n) => m.CreateValueReturn(base.ToString())));

        [CompilerGenerated]
        public bool TryAdd(ref int x, ref int y, out int z)
        {
            var _method = MethodBase.GetCurrentMethod();
            z = default;
            var _result = pipeline.Invoke(MethodInvocation.Create(this, _method, x, y, z));
            x = _result.Outputs.Get<int>("x");
            y = _result.Outputs.Get<int>("y");
            z = _result.Outputs.Get<int>("z");
            return (bool)_result.ReturnValue!;
        }

        [CompilerGenerated]
        public void TurnOn() => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod()));

        [CompilerGenerated]
        public event EventHandler TurnedOn
        {
            add => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
            remove => pipeline.Execute(MethodInvocation.Create(this, MethodBase.GetCurrentMethod(), value));
        }
    }
}

Support for default interface implementations

@kzu you asked me in #109 (comment) to open an issue regarding default interface implementations. I'm taking your word for them not currently being supported by this library.

At the IL level, there's nothing too surprising about them: they simply aren't marked abstract as interface methods usually are; they can have a method body like any other method. AFAIK interface methods are also no longer guaranteed to be declared public. Interfaces still cannot contain any instance fields, though IIRC they can now have static ones. Oh, and all of this is only supported starting with netcoreapp3.0 / netstandard2.1 runtimes.

At the reflection level, you'll notice that default implementations remain in the interface they're defined in. That is, a class implementing an interface with default impls doesn't inherit that implementation, you'll have to go look for it directly in the interface. Generally speaking, default impl integration with Reflection isn't terribly well done, there are some gotchas with GetInterfaceMap and detecting overrides etc. I could probably say more about this but will stop here for brevity.

At the C# language level, when implementing an interface, you can implement a method that has a default impl in the interface... but you don't have to. If you do choose to implement the method, then want to call the base implementation, you cannot just do base.Blah(), instead of base. you need to cast the this pointer to the interface with the desired default impl. I suppose the language designers chose this path to solve diamond inheritance issues where base might be ambiguous. They also came up with the concept of a "most specific override" rule.

If it's any help, I've recently added support for default interface impls to Moq 4 (see devlooped/moq#1130) — it works via CallBase, which will call the most specific override. Because DynamicProxy doesn't yet support them properly and Reflection also isn't terribly useful, I had to do a workaround using IL generation... it's not pretty, but may give you some insight into how those things work.

I hope your path using Roslyn will be a little less rocky. :-)

P.S. I should mention that instead of .CallBase() always picking the most specific override, one could consider a new .CallBase<TInterfaceWithImpl>() to allow user code to choose which base implementation should be called (as there could be several!). Meaning, there can be more than just one "target instance".

Improve argument collection API

Currently consuming the API is a tad confusing. Enumerating it will yield the ParameterInfos, rather than the values. Getting all values is tricky (requires a Select with a GetValue(name|index)), and setting values while enumerating is also cumbersome (need to keep the index around).

The main helpers and members around the IArgumentCollection itself are fine, but it seems like we really need to create a first-class Argument class (record?) that encapsulates both the value as well as the ParameterInfo, since in many non-trivial cases you need both to operate. This also gives as an opportunity to introduce (perhaps) an Argument<T> which would allows us to have box-free value types support (as long as the Get/Set are used with the right type consistently). This would also improve run-time performance significantly.

We could also centralize all type checking (conversion too?) in the new type too.

Add proper namespace to generated types

Currently all generated types go into a single namespace, Stunts, which can potentially cause conflicts, especially with #21. Since we don't need to concern ourselves with folder hierarchies and the file system anymore, we could make the naming convention much more resilient to these scenarios. We could assume Stunts.[entry type namespace].[stunt name], for example.

In the case of nested types, since the namespace would begin with Stunts., we could use the parent type name as a sub namespace too without risking collisions. It would work since the nested type would also live in a given namespace, and no other namespace would be allowed to have the same name (or type name). So in that case it would be Stunts.[parent type namespace].[parent type name].[stunt name].

Add support for ref returns

C# 7+ supports ref returns and so should Stunts.

This will likely involve slightly changing the generated code for a stunt method implementation so that the return value from a method invocation can be something that can be ref-returned.

Example:

        public ref int NumberAt(int index)
        {
            var returns = pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), index));

            return ref ((Ref<int>)returns.ReturnValue!).Value;
        }

Where Ref<T> would be something like:

    public class Ref<T>
    {
        T value;

        public Ref(T value) => this.value = value;

        public ref T Value => ref value;
    }

Allow modifying method invocation return values

It would be useful to be able to modify the returned values from a call to the base method implementation or the subsequent behaviors in the pipeline before returning to caller.

Currently, IMethodReturn has no public implementation and no mutating members that can be used for that, so it's pretty much forbidden (unless you implement the interface yourself).

We could either make the internal MethodReturn implementation public (like we do for MethodInvocation itself and/or add mutating methods like WithArguments, WithReturnValue and WithException. Since almost all our interface implementations are public, maybe that's the simplest approach here too?

From feedback by @stakx in #56

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.