ActionContainer is an implementation of the Service Agent pattern in written in C#. It takes advantage of the C# 4.0 dynamic feature to eliminate cluttering your code with empty message classes and also manipulate calls at runtime.
Usage of ActionContainer is straight forward. Start by creating a class that
implements the empty IActionProvider
interface with public methods.
public class SimpleProvider : IActionProvider
{
public void SayHello(string name)
{
Console.WriteLine("Hello, {0}", name);
}
public string GeneratePassword()
{
return "RaNdOmPaSsWoRd";
}
}
Inject an instance of the ServiceAgent class, and make calls to the registered methods
through the ServiceAgent
object.
public class Needy : IDependOnSomething
{
public dynamic ServiceAgent { get; set; }
public void DoSomething()
{
ServiceAgent.SayHello("Buddy");
string password = ServiceAgent.GeneratePassword();
}
}
Calling SayHello
with a string, will look up a registered method with the name
SayHello, takes a string argument, and has a void return type. Same behavior
goes for GeneratePassword
, except it returns a string.
Looking up methods by name and parameters doesn’t require creating a class (think SayHelloRequest) for each registered action.
The ServiceAgent is also return type aware. In the call string password =
ServiceAgent.GeneratePassword();
, a search is done for the GeneratePassword
that returns string. In this case, the SimpleProvider
has the only registered
GeneratePassword. Lets say we added another provider, the NumericProvider
as
follows:
public class NumericProvider: IActionProvider
{
public int GeneratePassword()
{
return 42;
}
}
Now calls to ServiceAgent.GeneratePassword();
can handle both int and string
return types with no special PasswordResponse or NumericPasswordResponse
wrapper classes.
string strpwd = ServiceAgent.GeneratePassword(); //returns "RaNdOmPaSsWoRd"
int numpwd = ServiceAgent.GeneratePassword(); //returns 42
The default method lookup ability of ActionContainer is just that: the default.
ActionContainer provides a hook into its resolving process simply by creating
implementations of the IActionListener
interface.
public interface IActionListener
{
bool CanHandle(ActionCallInfo callInfo);
bool Handle(ActionListenerContext context);
}
The CanHandle
method returns true if it has an opinion on the return value
of a call. If CanHandle
is true, Handle
allows the object to manipulate the
return value a value.
ActionCallInfo
includes the method name, named parameters, unnamed
parameters, and expected return type.
Using this information you could add helpful hooks such as a LogListener
.
public class LogListener : IActionListener
{
public bool CanHandle(ActionCallInfo callInfo)
{
var arg = callInfo
.NamedArguments
.FirstOrDefault(
x => x.Item1.Equals("log", StringComparison.OrdinalIgnoreCase)
);
if (arg == null || !(arg.Item2 is bool) || !(bool)arg.Item2)
return false;
var unnamed = callInfo.UnnamedArguments;
Console.Write("Calling {0}({1}) - Args: ", callInfo.MethodName, callInfo.ReturnType);
for (int i = 0; i < unnamed.Count; i++)
{
var val = unnamed[i];
Console.Write("input{0}= {1} ", i, val);
}
Console.WriteLine();
return false;
}
public bool Handle(ActionListenerContext context)
{
return false;
}
}
The LogListener
will watch for an argument named log with the value of
true. It will then write to the console the name of the call, return type, and
arguments. To put it to use, just use the ServiceAgent as follows:
ServiceAgent.SayHello("Buddy", log: true);
//Output:
//Calling SayHello() - Args: input0= Buddy
//Hello, Buddy
Because the ServiceAgent is a dynamic object, any number of named arguments can be passed to its method calls.
Another possible hook would be a StringTrimListener
. Any arguments that are strings
can be trimmed before getting passed to the handling method.
public class StringTrimListener : IActionListener
{
public bool CanHandle(ActionCallInfo callInfo)
{
var unnamed = callInfo.UnnamedArguments;
for (int i =0; i < unnamed.Count; i++)
{
var val = unnamed[i] as string;
if (val != null)
unnamed[i] = val.Trim();
}
return false;
}
public bool Handle(ActionListenerContext context)
{
return false;
}
}
No change in code is needed to have calls to the ServiceAgent start using the
StringTrimListener
. Just make a call on something with a string with hanging
spaces.
ServiceAgent.SayHello(" Friend ", log: true);
//Output:
//Hello, Friend
ActionContainer has some requirements that you must keep in mind while using it. Most deal with determining types.
First, when doing variable assignment you must explicitly provide the type when
returning values. Using var
will result in unfriendly behavior.
var result = ServiceAgent.GeneratePassword(); //BAD!
Second, passing null values as parameters limits lookup. This has to do with null not carrying any type information other than object.
string name = null;
ServiceAgent.SayHello(name); //Less than favorable
Finally, ActionContainer is slow (~10x) compared to direct method calls. Using it in a long running loop will have noticeable performance hits. However, in response to user actions, incoming web request, etc, the difference is negligible.
It’s on Github. Go and grab it with Git.
$ git clone git://github.com/statianzo/ActionContainer.git
13 Aug 2010