Categories
Uncategorized

Using Interfaces to Enable ScriptLink Backwards Compatibility

I first started working with myAvatar ScriptLink during the days of the OptionObject (v1). Shortly after, an updated version, OptionObject2, was released with some additional valuable properties. I completely missed the announcement of this version and didn’t stumble on it until well after the OptionObject2015 version was released. So I was using OptionObject in production, but I wanted to upgrade to a newer release without writing duplicate code. Inheritance became my solution and a foundation of the design of the AvatarScriptLink.NET library.

I first started working with myAvatar ScriptLink during the days of the OptionObject (v1). Shortly after, an updated version, OptionObject2, was released with some additional valuable properties. I completely missed the announcement of this version and didn’t stumble on it until well after the OptionObject2015 version was released. So I was using OptionObject in production, but I wanted to upgrade to a newer release without writing duplicate code. Inheritance became my solution and a foundation of the design of the AvatarScriptLink.NET library.

The Concept

Each version of of the OptionObject are closely related. The shared properties are identical between each version with the newer versions adding new properties. So in a sense, OptionObject2 inherited the majority of its properties from OptionObject and OptionObject2015 the same from OptionObject2.

In this section, we will explore the concept from the perspective of C# specifically. Please note the concept will apply to other languages, but implementation may look very different.

Interfaces

An interface defines the properties and/or methods a class must implement. This becomes helpful in that if you have multiple classes that inherit the same interface, you can create a method parameter that receives the interface instead of having to create a method for each class. I have started placing interfaces in a sub-folder named Advanced where the classes are defined (e.g., RS.ScriptLinkDemo.Objects.Advanced). Let’s see what this might look like.

OptionObject

In our initial step we created the OptionObject2015 class. If we were to define the OptionObject interface, it would look like this. Notice that properties such as ServerName and SessionToken are not defined here. They were introduced in the later versions.

public interface IOptionObject
{
    string EntityID { get; set; }
    double EpisodeNumber { get; set; }
    double ErrorCode { get; set; }
    string ErrorMesg { get; set; }
    string Facility { get; set; }
    List<FormObject> Forms { get; set; }
    string OptionId { get; set; }
    string OptionStaffId { get; set; }
    string OptionUserId { get; set; }
    string SystemCode { get; set; }
}

OptionObject2

Now let’s look at the interface for OptionObject2. The colon states that IOptionObject2 inherits from IOptionObject. This means that when we implement a class that inherits IOptionObject2 we will have to implement the properties defined by both interfaces.

public interface IOptionObject2 : IOptionObject
{
    string NamespaceName { get; set; }
    string ParentNamespace { get; set; }
    string ServerName { get; set; }
}

OptionObject2015

You can probably guess now what the IOptionObject2015 interface will look like.

public interface IOptionObject2015 : IOptionObject2
{
    string SessionToken { get; set; }
}

Update Our Classes

Go ahead and create the above interfaces and place them in your class library project.

Next we will update our OptionObject2015 class to inherit its interface and add the OptionObject and OptionObject2 classes (if you haven’t already).

public class OptionObject2015 : IOptionObject2015
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string ParentNamespace { get; set; }
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
    public string SessionToken { get; set; }
}
public class OptionObject2 : IOptionObject2
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string ParentNamespace { get; set; }
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
}
public class OptionObject : IOptionObject
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string SystemCode { get; set; }
}

Now we have the interfaces defined and the concrete classes inheriting them. We don’t gain anything from this yet, so let’s look at our methods that use these objects.

Updating the Commands

Let’s take a look at our HelloWorld command. Notice it too implements an interface. This allows our API execute any command we create regardless of concrete class. So because our commands are expected by the IRunScriptCommand interface to use the concrete OptionObject2015 class, we will need to update it to use a common interface instead. In this case each version of the OptionObject inherits from IOptionObject.

public interface IRunScriptCommand
{
    IOptionObject Execute();
}

We’re now telling our commands that they must return an IOptionObject instead of specifically an OptionObject2015. This mean that the commands will be able to return all three versions now.

Here’s what the HelloWorld command would look like at minimum. Notice that while it returns an IOptionObject it is still dependent upon (constructed with) an OptionObject2015.

public class HelloWorld : IRunScriptCommand
    {
        private readonly OptionObject2015 _optionObject2015;

        public HelloWorld(OptionObject2015 optionObject2015)
        {
            _optionObject2015 = optionObject2015;
        }

        public IOptionObject Execute()
        {
            return new OptionObject2015()
            {
                EntityID = _optionObject2015.EntityID,
                EpisodeNumber = _optionObject2015.EpisodeNumber,
                ErrorCode = 3,
                ErrorMesg = "Hello, World!",
                Facility = _optionObject2015.Facility,
                NamespaceName = _optionObject2015.NamespaceName,
                OptionId = _optionObject2015.OptionId,
                OptionStaffId = _optionObject2015.OptionStaffId,
                OptionUserId = _optionObject2015.OptionUserId,
                ParentNamespace = _optionObject2015.ParentNamespace,
                ServerName = _optionObject2015.ServerName,
                SystemCode = _optionObject2015.SystemCode,
                SessionToken = _optionObject2015.SessionToken
            };
        }
    }

To allow this command to access the older versions we need to update our constructor as well. You will notice as you step through the changes some compatibility issues that have to be addressed.

public class HelloWorld : IRunScriptCommand
{
    private readonly IOptionObject _optionObject;

    public HelloWorld(IOptionObject optionObject)
    {
        _optionObject = optionObject;
    }

    public IOptionObject Execute()
    {
        return new OptionObject2015()
        {
            EntityID = _optionObject.EntityID,
            EpisodeNumber = _optionObject.EpisodeNumber,
            ErrorCode = 3,
            ErrorMesg = "Hello, World!",
            Facility = _optionObject.Facility,
            //NamespaceName = _optionObject.NamespaceName,      // NamespaceName not available in IOptionObject
            OptionId = _optionObject.OptionId,
            OptionStaffId = _optionObject.OptionStaffId,
            OptionUserId = _optionObject.OptionUserId,
            //ParentNamespace = _optionObject.ParentNamespace,  // ParentNamespace not available in IOptionObject
            //ServerName = _optionObject.ServerName,            // ServerName not available in IOptionObject
            SystemCode = _optionObject.SystemCode,
            //SessionToken = _optionObject.SessionToken         // SessionToken not available in IOptionObject
        };
    }
}

The good news is that this will now compile, accept all three versions, and return all three versions, but we lose the values of the properties of the newer versions. Now we have compatibility with OptionObject, but lost it for OptionObject2015. In construction they are fine but the expected values are not there.

A Solution To Our Regression

If we were to follow these changes through our code would compile and our unit tests will pass because we are not testing for those values. However, once implemented users would like receive an error due to an invalid return OptionObject2015. A solution to this is to use the newest version’s interface for each of them instead.

public class OptionObject2 : IOptionObject2015
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string ParentNamespace { get; set; }
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
    public string SessionToken { get; set; }
}
public class OptionObject : IOptionObject2015
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string ParentNamespace { get; set; }
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
    public string SessionToken { get; set; }
}

Now the IOptionObject2015 interface is a common interface for each, but the OptionObject and OptionObject2 WSDL will not be valid because there are additional properties that shouldn’t be there. .NET has an annotation that can help here from the System.Xml.Serialization namespace that will enable us to ignore the properties that are not for those classes.

public class OptionObject2 : IOptionObject2015
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    public string ParentNamespace { get; set; }
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
    [XmlIgnore]
    public string SessionToken { get; set; }
}
public class OptionObject : IOptionObject2015
{
    public string EntityID { get; set; }
    public double EpisodeNumber { get; set; }
    public double ErrorCode { get; set; }
    public string ErrorMesg { get; set; }
    public string Facility { get; set; }
    public List<FormObject> Forms { get; set; }
    [XmlIgnore]
    public string NamespaceName { get; set; }
    public string OptionId { get; set; }
    public string OptionStaffId { get; set; }
    public string OptionUserId { get; set; }
    [XmlIgnore]
    public string ParentNamespace { get; set; }
    [XmlIgnore]
    public string ServerName { get; set; }
    public string SystemCode { get; set; }
    [XmlIgnore]
    public string SessionToken { get; set; }
}

Now our WSDLs for the OptionObject and OptionObject2 will be compliant. Let’s get back to our IRunScriptCommand and HelloWorld command.

public interface IRunScriptCommand
{
    IOptionObject2015 Execute();
}
public class HelloWorld : IRunScriptCommand
{
    private readonly IOptionObject2015 _optionObject;

    public HelloWorld(IOptionObject2015 optionObject)
    {
        _optionObject = optionObject;
    }

    public IOptionObject2015 Execute()
    {
        return new OptionObject2015()
        {
            EntityID = _optionObject.EntityID,
            EpisodeNumber = _optionObject.EpisodeNumber,
            ErrorCode = 3,
            ErrorMesg = "Hello, World!",
            Facility = _optionObject.Facility,
            NamespaceName = _optionObject.NamespaceName,
            OptionId = _optionObject.OptionId,
            OptionStaffId = _optionObject.OptionStaffId,
            OptionUserId = _optionObject.OptionUserId,
            ParentNamespace = _optionObject.ParentNamespace,
            ServerName = _optionObject.ServerName,
            SystemCode = _optionObject.SystemCode,
            SessionToken = _optionObject.SessionToken
        };
    }
}

Better. Now our command can receive and return any version of the OptionObject without losing required values and maintaining compliant WSDLs. Now we’ll repeat the change for our DefaultCommand.

public class DefaultCommand : IRunScriptCommand
{
    private readonly IOptionObject2015 _optionObject2015;
    private readonly string _parameter;

    public DefaultCommand(IOptionObject2015 optionObject2015, string parameter)
    {
        _optionObject2015 = optionObject2015;
        _parameter = parameter;
    }

    public IOptionObject2015 Execute()
    {
        string message = "Error: There is no command matching the parameter '" + _parameter + "'. Please verify your settings.";

        return new OptionObject2015()
        {
            EntityID = _optionObject2015.EntityID,
            EpisodeNumber = _optionObject2015.EpisodeNumber,
            ErrorCode = 3,
            ErrorMesg = message,
            Facility = _optionObject2015.Facility,
            NamespaceName = _optionObject2015.NamespaceName,
            OptionId = _optionObject2015.OptionId,
            OptionStaffId = _optionObject2015.OptionStaffId,
            OptionUserId = _optionObject2015.OptionUserId,
            ParentNamespace = _optionObject2015.ParentNamespace,
            ServerName = _optionObject2015.ServerName,
            SystemCode = _optionObject2015.SystemCode,
            SessionToken = _optionObject2015.SessionToken
        };
    }
}

Excellent. If we have set this up correctly then our solution should compile and our Unit Tests should pass (though code coverage is very low). We still have two remaining issues. Our CommandSelector factory only accepts OptionObject2015 objects and our v1 and v2 endpoints don’t implement our commands.

Updating the Factory

This step is fairly straightforward. We’re going to change the CommandSelector GetCommand method to accept the interface instead of the concrete class.

public static IRunScriptCommand GetCommand(IOptionObject2015 optionObject, string parameter)
{
    switch (parameter)
    {
        case "HelloWorld":
            return new HelloWorld(optionObject);
        default:
            return new DefaultCommand(optionObject, parameter);
    }
}

Oops! Now we have a compile error with our web service endpoints.

Updating the Web Service Endpoints

This steps is more interesting than the factory, but far less so then our library and command changes. Let’s take a look at our v3 endpoint.

[WebMethod]
public string GetVersion()
{
    var command = new GetVersion();
    return command.Execute();
}
 
[WebMethod]
public OptionObject2015 RunScript(OptionObject2015 optionObject2015, string parameter)
{
    IRunScriptCommand command = CommandSelector.GetCommand(optionObject2015, parameter);
    return command.Execute();
}

You’ll notice we implement the OptionObject2015 concrete class as both an incoming parameter and as the return object. Like before we may be tempted to switch those to the new interface to resolve the compilation error but that ultimately will not work. The WSDL must use the concrete classes.

Our solution is to convert the command’s return object to the version we need like this.

[WebMethod]
public string GetVersion()
{
    var command = new GetVersion();
    return command.Execute();
}
 
[WebMethod]
public OptionObject2015 RunScript(OptionObject2015 optionObject2015, string parameter)
{
    IRunScriptCommand command = CommandSelector.GetCommand(optionObject2015, parameter);
    return (OptionObject2015)command.Execute();
}

Now we are compliant with the WSDL, our commands and factory can accept all three versions, and the solution will compile. Now we can ad our update our v1 (OptionObject) and v2 (OptionObject2) endpoints.

[WebMethod]
public string GetVersion()
{
    var command = new GetVersion();
    return command.Execute();
}
 
[WebMethod]
public OptionObject2 RunScript(OptionObject2 optionObject2, string parameter)
{
    IRunScriptCommand command = CommandSelector.GetCommand(optionObject2, parameter);
    return (OptionObject2)command.Execute();
}
[WebMethod]
public string GetVersion()
{
    var command = new GetVersion();
    return command.Execute();
}
 
[WebMethod]
public OptionObject RunScript(OptionObject optionObject, string parameter)
{
    IRunScriptCommand command = CommandSelector.GetCommand(optionObject, parameter);
    return (OptionObject)command.Execute();
}

There we go. Now our solution can support all three versions of the OptionObject.

Why Do This At All?

When I was originally exploring this ability there wasn’t a lot of interest by other developers I collaborated with. The most obvious argument is that if you are already on the newest version “why add support for the older versions?” You don’t. The purpose of this is primarily to show you can and how you might do it.

The primary context for utilizing this to bring a legacy project up to the new version while still supporting the legacy version you are on. Let’s say you are using the OptionObject2, but you now need to update to OptionObject2015 to leverage Avatar Web Services. With this design you can create a separate OptionObject2015 endpoint that shares the same code (business logic) as production without breaking your existing use. It gives you a fallback if cutting over to the new endpoint fails for whatever reason, without redeploying or rolling back code. It also helps guarantee that the APIs will behave in a consistent manner when you do the migration. If it works on OptionObject2 it should just work on OptionObject2015. Additionally, this design could aid in future upgrades if Netsmart releases a v4 OptionObject.

What I especially like about this is the learning opportunity. Exploring this helped open up new solutions in other areas of these projects as well as other unrelated ones. I know we often get too busy to explore different ways to solve a problem, but taking a few moments here and there could save you a lot of time later.

The AvatarScriptLink.NET library is already configured to support this so you can make your methods backwards compatible without having to mess with the classes. Just reference the RarelySimple.AvatarScriptLink.Object.Advanced namespace to access the IOptionObject2015 interface.

I hope you found this interesting. What other scenarios do you see where interfaces and inheritance have helped with your ScriptLink solution?

One reply on “Using Interfaces to Enable ScriptLink Backwards Compatibility”

Comments are closed.