Lately I've been playing around with USB led lights in .Net. I wanted the animations to be separated from my code. Wouldn't it be great if you could define what animations are executed in a JSON file? And map it to code?
Normally one would build a mapper that does the conversion of the JSON commands to the methods. I like to use a more generic approach. I've created a small utility class that executes commands by mapping and executing them as a method of the object. The solution is workable for a host of scenarios:
- You can use it to map a JSON RPC command from a webservice on an object, creating a more generic service.
- You can now create a JSON file with USB led animations specification that need to be executed when a certain event occurs.
- Simplifying the mapping between commands and objects in a generic way.
Nuget
Before you start, there is a Nuget package. (Link to GitHub at the end of the blog.)
- Install-Package KeesTalksTech.Utilities.Rpc -Version 0.3.2
- dotnet add package KeesTalksTech.Utilities.Rpc --version 0.3.2
- <PackageReference Include="KeesTalksTech.Utilities.Rpc" Version="0.3.2" />
Remote Timer Example
I'll show you how things work using an example. Let's create a remotely controlled timer. Commands on the timer are actions like start and stop.
/// <summary>
/// Indicates the object implements a timer.
/// </summary>
public interface IRemoteTimer
{
/// <summary>
/// Gets a value indicating whether this instance is running.
/// </summary>
/// <value>
/// <c>true</c> if this instance is running; otherwise, <c>false</c>.
/// </value>
bool IsRunning
{
get;
}
/// <summary>
/// Gets the ticks total.
/// </summary>
/// <returns>The ticks.</returns>
int GetTicksTotal();
/// <summary>
/// Sets the interval.
/// </summary>
/// <param name = "newInterval">The new interval.</param>
void SetInterval(double newInterval);
/// <summary>
/// Starts this instance.
/// </summary>
void Start();
/// <summary>
/// Stops this instance.
/// </summary>
void Stop();
}
Let's implement the class:
using System;
using System.Timers;
public class RemoteTimer : IRemoteTimer, IDisposable
{
private Timer timer;
private int ticks = 0;
/// <summary>
/// Gets the interval.
/// </summary>
/// <value>
/// The interval.
/// </value>
public double Interval
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the <see cref = "RemoteTimer"/> class.
/// </summary>
/// <param name = "interval">The interval.</param>
public RemoteTimer(double interval)
{
SetInterval(interval);
}
/// <summary>
/// Sets the interval.
/// </summary>
/// <param name = "interval">The interval.</param>
public void SetInterval(double interval)
{
var isRunning = (timer?.Enabled).GetValueOrDefault();
timer?.Stop();
timer?.Dispose();
timer = new Timer(interval);
timer.Elapsed += (s, e) => ticks++;
Interval = interval;
if (isRunning)
{
Start();
}
}
/// <summary>
/// Starts this instance.
/// </summary>
public void Start()
{
timer.Start();
}
/// <summary>
/// Stops this instance.
/// </summary>
public void Stop()
{
timer.Stop();
}
/// <summary>
/// Gets a value indicating whether this instance is running.
/// </summary>
/// <value>
/// <c>true</c> if this instance is running; otherwise, <c>false</c>.
/// </value>
public bool IsRunning
{
get
{
return timer.Enabled;
}
}
/// <summary>
/// Gets the ticks total.
/// </summary>
/// <returns>
/// The ticks.
/// </returns>
public int GetTicksTotal()
{
return ticks;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
timer.Dispose();
}
}
Safety first
Classes have many (inherited) methods. There are some methods you most likely never ever want to expose (like the Dispose
method). That's why the Interpertation
is always done on an interface. This is especially important when you are using this project as part of RPC construction.
To execute JSON commands we'll need to create an IInterpreter
based on an instance of the timer and the IRemoteTimer
interface:
var json = "";
var timer = new RemoteTimer(10);
var interpretor = Interpretation.Create<IRemoteTimer>(timer);
Executing a method
A method is defined as a JSON object with a method-name
property. The system uses it to map it to a method.
/* Start the timer and assert it is running */
json = @"{ ""method-name"": ""Start"" }";
interpretor.Execute(json);
Assert.IsTrue(timer.IsRunning);
Got parameters?
Parameters are defined as properties as well. Just add their names and values to JSON:
/* Set a new interval: */
json = @"{ ""method-name"": ""SetInterval"", ""newInterval"": 20 }";
interpretor.Execute(json);
Assert.AreEqual(20, timer.Interval);
A series of methods
If you want to execute more than one method, just add the command in an array:
/* Stop the timer, set a new interval and restart it: */
json = @"[{ ""method-name"": ""Stop""},
{ ""method-name"": ""SetInterval"", ""newInterval"": 20 },
{ ""method-name"": ""Start""}]";
interpretor.Execute(json);
Assert.AreEqual(20, timer.Interval);
How about retrieving information?
Thought of that as well! Any results are returned as an object or an array of objects (in case of a series):
/* Get tick information from timer */
json = @"{ ""method-name"": ""GetTicksTotal""}";
var ticks = interpretor.Execute(json);
Assert.IsNotNull(ticks);
Assert.IsInstanceOfType(ticks, typeof(int));
Assert.IsTrue(Convert.ToInt32(ticks) > 0);
How about extension methods?
Extension methods are awesome. Let's define a Pause
method:
public static class IRemoteTimerExtensions
{
/// <summary>
/// Pauses the specified timer.
/// </summary>
/// <param name = "timer">The timer.</param>
public static void Pause(this IRemoteTimer timer)
{
if (timer.IsRunning)
{
timer.Stop();
}
else
{
timer.Start();
}
}
}
To allow extensions methods to be considered, they'll need to be given to the Create
:
/* Pause the timer twice and assert running state */
var json = "";
var timer = new RemoteTimer(10);
var interpretor = Interpretation.Create<IRemoteTimer>(
timer,
typeof(IRemoteTimerExtensions)
);
timer.Start();
json = @"{ ""method-name"": ""Pause"" }";
interpretor.Execute(json);
Assert.IsFalse(timer.IsRunning);
json = @"{ ""method-name"": ""Pause"" }";
interpretor.Execute(json);
Assert.IsTrue(timer.IsRunning);
Wrap up
The code has potential to solve a host of problems. If you like to fork it, please check GitHub and let me know what you think. The code used in the example can be found here.