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

[C# Port] Refactor the action handling system #212

Closed
Tracked by #82
singhk97 opened this issue Jun 27, 2023 · 9 comments
Closed
Tracked by #82

[C# Port] Refactor the action handling system #212

singhk97 opened this issue Jun 27, 2023 · 9 comments
Assignees
Labels
dotnet Change/fix applies to dotnet. If all three, use the 'JS & dotnet & Python' label

Comments

@singhk97
Copy link
Collaborator

No description provided.

@singhk97 singhk97 mentioned this issue Jun 27, 2023
13 tasks
@singhk97 singhk97 added the dotnet Change/fix applies to dotnet. If all three, use the 'JS & dotnet & Python' label label Jun 28, 2023
@singhk97
Copy link
Collaborator Author

singhk97 commented Jul 4, 2023

The current action delegate is quite rigid and has a few flaws. Look at the default actions and how the handler functions are implemented here:

// TODO: Resolve this issue before private preview. Need more research and thinking. How are developers going to use this?
// 1. Unused parameters, 2. Making the data parameter type more explicit and not just "object".
public class DefaultActions<TState> where TState : TurnState

The flaws are:

  1. Unused parameters - the delegate signature has a fixed set of parameters which might not be used in the handler implementation. We ideally don't want users to write functions with parameters that they won't be using. Possible solutions:

    • Supporting multiple delegate types - this is what SK does. They have at least 15 different delegate types that they support. Those delegate signatures are a combination of a 2 parameters and a few return types. It might be harder for us to do this as we support 5 parameter
    • Having a single parameter object - This is problematic as it's unclear which properties are required for an arbitrary function.
    • Dynamic delegates - It might be possible to have a dynamic delegates type. Take a look at: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview?view=aspnetcore-7.0.

Image

The delegate type for the MapGet method is dynamic and dependent on the previous argument. They might be using the reflection api to figure out the parameters and populate the delegate accordingly.

  1. Making the data parameter type more explicit and not just "object" - We have it at that right now because the parameter must be able to take all kinds of types. However, we want to take advantage of the strongly-typedness of C#. Generic types will probably fix this.

@swatDong
Copy link
Contributor

swatDong commented Jul 5, 2023

@singhk97 @Stevenic , I have another idea to use parameter attribute to support dynamic argument. This idea comes from Azure Functions Binding that Azure Functions support dynamic argument by different parameter attributes, such as:
image
image

For example, we provide parameter attributes like TurnContextAttribute, TurnStateAttribute, ActionDataAttribute, ActionNameAttribute. Then it's up to action method to use none or one or more of those attributes.

    public class MyInstance
    {
        [Action("FullAction")]
        public void MyFullActionHandler(
            [TurnContext] TurnContext context,
            [TurnState(typeof(MyTurnState))] MyTurnState state,
            [ActionData(typeof(MyData))] MyData data,
            [ActionName] string actionName)
        {
            return;
        }

        [Action("PartialAction")]
        public void MyPartialActionHandler([ActionName] string actionName, [ActionData(typeof(MyData))] MyData data)
        {
            return;
        }

        [Action("DoNothing")]
        public void DoNothingActionHandler()
        {
            return;
        }
    }

By the way, Generic Attributes is just proposed, so for now we may need to pass type to the attribute.

@singhk97
Copy link
Collaborator Author

singhk97 commented Jul 5, 2023

Adding @vule-msft to the discussion.

@stephentoub
Copy link
Member

Supporting multiple delegate types - this is what SK does. They have at least 15 different delegate types that they support. Those delegate signatures are a combination of a 2 parameters and a few return types. It might be harder for us to do this as we support 5 parameter

It's what SK used to do. It's no longer what it does. It now supports any number of arguments of any relevant type.
microsoft/semantic-kernel#1195

@swatDong
Copy link
Contributor

swatDong commented Jul 7, 2023

Supporting multiple delegate types - this is what SK does. They have at least 15 different delegate types that they support. Those delegate signatures are a combination of a 2 parameters and a few return types. It might be harder for us to do this as we support 5 parameter

It's what SK used to do. It's no longer what it does. It now supports any number of arguments of any relevant type. microsoft/semantic-kernel#1195

Thanks for this reminder. It's great to know there's such use pattern in SK.
BTW, I went through the code and noticed that it's using MethodInfo.Invoke(...) to call user-defined native method.
Is there any usage or performance consideration on choosing MethodInfo.Invoke or Expression.Compile?

@stephentoub
Copy link
Member

Is there any usage or performance consideration on choosing MethodInfo.Invoke or Expression.Compile?

Expression.Compile is using reflection emit under the covers to generate custom IL. It's thus much more expensive on first use, in exchange for having invocation performance be much faster. If you're going to be executing the call a lot and the overhead of those calls matter, it can be a worthwhile tradeoff. The difference is measured in nanoseconds, though, so you really do need to be making many, many calls to the resulting delegate for the startup costs to be worth it.

Also, recent versions of .NET optimize MethodInfo.Invoke further. In .NET 7, the implementation can actually use reflection emit under the covers to achieve a similar optimization. See https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#reflection for more details.

@swatDong
Copy link
Contributor

Expression.Compile is using reflection emit under the covers to generate custom IL. It's thus much more expensive on first use, in exchange for having invocation performance be much faster. If you're going to be executing the call a lot and the overhead of those calls matter, it can be a worthwhile tradeoff. The difference is measured in nanoseconds, though, so you really do need to be making many, many calls to the resulting delegate for the startup costs to be worth it.

Also, recent versions of .NET optimize MethodInfo.Invoke further. In .NET 7, the implementation can actually use reflection emit under the covers to achieve a similar optimization. See https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#reflection for more details.

This is really helpful. Thanks a lot!

@swatDong
Copy link
Contributor

@stephentoub , the sample and doc you provided are really helpful, and I've implemented new attribute-based handler.

Discussed with @singhk97 and one of the good suggestions is to provide an analyzer to check the parameter type of the attribute.

I went through SK repo but did not find any similar part. In addition, the only similar analyzer I found is from several years ago.

Is there any suggested getting-started or potential risk for providing analyzer to check attribute-binding parameter type?

Thanks again!

swatDong added a commit that referenced this issue Jul 12, 2023
Refactor action handling system to support dynamic action import with
necessary parameters, resolves
#212
- Action method can use parameter attributes `ActionTurnContext`,
`ActionTurnState`, `ActionEntities`, `ActionName` to get inputs.
- Support `void` / `bool` / `Task` / `Task<bool>` / `ValueTask` /
`ValueTask<bool>` return types.
- Have runtime type checker.

For example,
``` csharp
        [Action("action1")]
        public void MyAction(
            [ActionTurnContext] ITurnContext context,
            [ActionTurnState] TState state,
            [ActionEntities] MyDataType entities,
            [ActionName] string actionName) { ... }
```

This PR may have conflict with the turn state one. I'll merge and
resolve conflicts after #232
merged.
For code analyzer, created
#237 to track to not block
the action system itself or sample work.
@stephentoub
Copy link
Member

Is there any suggested getting-started or potential risk for providing analyzer to check attribute-binding parameter type?

There are tons of analyzers implemented in https://github.com/dotnet/roslyn-analyzers... you should be able to find some good fodder there. The risk is simply that you're writing code that will run as part of compilation, e.g. if you have a bug and it's really slow, that will significantly slow down compile times.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dotnet Change/fix applies to dotnet. If all three, use the 'JS & dotnet & Python' label
Projects
None yet
Development

No branches or pull requests

3 participants