Can I make a template, or generate a bunch of almost-identical properties in C#?

Viewed 99

I'm writing a C# class to drive a serial hardware device, which has a bunch of double settings. The underlying driver library has a bunch of setter and getter functions for double values, and I want to wrap them in properties with some error handling. Can I generate these properties with some sort of template?

So I want to end up with something like:

public double Acceleration
{
    get
    {
        int errorCode = _device.AC_Get(out double value, out string errorString);
        if (errorCode != 0)
            throw new Exception(errorString);
        return value;
    }
    set
    {
        int errorCode = _device.AC_Set(value, out string errorString);
        if (errorCode != 0)
            throw new Exception(errorString);
    }
}

And make a template so I can define a bunch of properties like this:

public double Acceleration : DoublePropertyTemplate(_device.AC_Set, _device.AC_Get)
public double Velocity : DoublePropertyTemplate(_device.VEL_Set, _device.VEL_Get)
public double Position : DoublePropertyTemplate(_device.POS_Set, _device.POS_Get)

Obviously I can just write the full property definition a million times, but it would be educational, and maybe more efficient to learn how to generate them.

2 Answers

I'm glad you mentioned "a more C# way to do it." While making a template to generate code can be useful and extremely efficient, this example seems to be a poor use case for a literal template approach; here's a few reasons why:

  1. It's automating repetition into your code base. If you have n of these properties, the majority of the code is just repeated n times. Before you know it, your code is soaking WET.
  2. The code generated from the template needs to be updated manually at all points of use. Unfortunately, you cannot auto-refactor all the code that used that template; can you remember all the places it was used??
  3. It's overlooking basic OOD/P concepts.

To me, "a more C# way to do it" means code. And I think you could achieve this with code though it may seem a little indirect and clunky due to the use of a delegate. Nonetheless, I think it's still superior to the template approach so here it goes...

Your code is exactly the same for all these double properties which makes the majority of it error handling boilerplate. To get around that, we need a way to abstract the actual method being called on the device; enter delegates.

Below, I have a class that takes as its constructor arguments two delegates that match the methods in the device. I then expose a double Value property that is used to keep the boilerplate common to all properties. To use it, you'll just pass the corresponding device methods to the DoubleProp and you should be good to go. Notice that this code will be used by all DoubleProps; the template is this class definition.

class DoubleProp
{
    public delegate int Getter(out double value, out string errorString);
    public delegate int Setter(double value, out string errorString);

    private Getter _getter;
    private Setter _setter;

    public double Value
    {
        get
        {
            CheckErrorCode(
                _getter(out double value, out string errorString), 
                errorString
            );
            return value;
        }

        set
        {
            CheckErrorCode(
                _setter(value, out string errorString), 
                errorString
            );
        }
    }

    public DoubleProp(Getter getter, Setter setter)
    {
        _getter = getter;
        _setter = setter;
    }

    private void CheckErrorCode(int errorCode, string errorString)
    {
        if (errorCode != 0)
        {
            throw new Exception(errorString);
        }
    }

    // Make getting the value a little prettier...
    public static implicit operator double(DoubleProp dp) => dp.Value;
}
/* Usage:
   // Given this Acceleration property...
   public DoubleProp Acceleration { get; set; }

   // You could construct it like so provided
   // _device is available...
   Acceleration = new DoubleProp(_device.AC_Get, _device.AC_Set);
*/

I did implement one solution using code generation as suggested in a comment on the question, by @GSerg. He linked to this Visual Studio Documentation on Code Generation.

It works well, but I think @CheifTwoPencils has a better answer.

So I added a Text Template to my project, and used it to generate an abstract class full of properties for my actual device class to inherit from. In the Text Template I listed my properties as a list of string tuples, with the property name, type, calling function, and a doc string.

Here's my template:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

<# var AxisProperties = new List<(string,string,string,string)> {
    ("Acceleration",        "AC",   "double",   "Acceleration Setting for Axis"), 
    ("EStopDeceleration",   "AE",   "double",   "E-Stop Acceleration Setting For Axis")
}; #>

// This code is generated from NewportESP301 Property Template.tt

namespace NewportESP30xController
{
    public abstract class NewportESP301AxisProperties : ESPMotionInterfaceBase
    {
        public abstract int AxisNumber { get; }

    <# // This code runs in the text template:
    foreach ((string propertyName, string codeName, string dataType, string description) in AxisProperties)  { #>
    /// <summary>
        /// <#= description #>
        /// </summary>
        public <#= dataType #> <#= propertyName #>
        {
            get
            {
                CheckErrorStatus(_controllerDevice.<#= codeName #>_Get(AxisNumber, out <#= dataType #> value, out string errorString), errorString);
                return value;
            }
            set
            {
                // TODO: Validate Value?
                CheckErrorStatus(_controllerDevice.<#= codeName #>_Set(AxisNumber, value, out string errorString), errorString);
            }
        }

    <# } #>
}
}

and here's the C# that it generates:

// This code is generated from NewportESP301 Property Template.tt

namespace NewportESP30xController
{
    public abstract class NewportESP301AxisProperties : ESPMotionInterfaceBase
    {
        public abstract int AxisNumber { get; }

        /// <summary>
        /// Acceleration Setting for Axis
        /// </summary>
        public double Acceleration
        {
            get
            {
                CheckErrorStatus(_controllerDevice.AC_Get(AxisNumber, out double value, out string errorString), errorString);
                return value;
            }
            set
            {
                // TODO: Validate Value?
                CheckErrorStatus(_controllerDevice.AC_Set(AxisNumber, value, out string errorString), errorString);
            }
        }

        /// <summary>
        /// E-Stop Acceleration Setting For Axis
        /// </summary>
        public double EStopDeceleration
        {
            get
            {
                CheckErrorStatus(_controllerDevice.AE_Get(AxisNumber, out double value, out string errorString), errorString);
                return value;
            }
            set
            {
                // TODO: Validate Value?
                CheckErrorStatus(_controllerDevice.AE_Set(AxisNumber, value, out string errorString), errorString);
            }
        }
    }
}
Related