Implicit Operators

Implicit Operators in C# allow a class to implicitly convert to another class or type. In the example below, we can take a string like “local/TPALARM_v3” and cast it as an AlarmDesignation. This then gives us the ability to use all of our methods defined on this class.

namespace Honeywell.APMINDMSFT.ApmDataIngestLib
{
    using System;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;

    [TypeConverter(typeof(AlarmDesignationConverter))]
    public class AlarmDesignation : IEquatable<AlarmDesignation>
    {
        private string type;
        private string scope;
        private int version;

        public AlarmDesignation(string raw)
        {
            this.Scoped = raw;
        }

        public bool HasType => !string.IsNullOrEmpty(this.type);
        public bool HasVersion => this.version > 0;
        public bool HasScope => !string.IsNullOrEmpty(this.scope);

        public int Version => this.version;
        public string Scope => this.scope;

        public static implicit operator string(AlarmDesignation value)
        {
            return value.Scoped;
        }

        public static implicit operator AlarmDesignation(string value)
        {
            return new AlarmDesignation(value);
        }

        public override string ToString()
        {
            return this.Scoped;
        }

        // ex. TPALARM
        public string Type
        {
            get => this.type;
            private set
            {
                if (!value.Contains("/") && !value.Contains("\\") && !value.Contains("_"))
                {
                    this.type = value;
                }
            }
        }

        // ex. TPALARM_v3
        public string Versioned
        {
            get => this.HasVersion ? $"{this.type}_v{this.version}" : this.type;
            private set
            {
                var parts = value.Split("_", 2);
                if (parts.Length == 1)
                {
                    this.Type = value;
                }
                else if (int.TryParse(parts[1][1..], out var version))
                {
                    this.Type = parts[0];
                    if (version > 0)
                    {
                        this.version = version;
                    }
                }
                else
                {
                    this.Type = parts[0];
                }
            }
        }

        // ex. local/TPALARM_v3
        public string Scoped
        {
            get => this.HasScope ? $"{this.scope}/{this.Versioned}" : this.Versioned;
            private set
            {
                var parts = value.Split("/", 2);
                if (parts.Length == 1)
                {
                    this.Versioned = value;
                }
                else if (parts[0].Equals("local", StringComparison.InvariantCultureIgnoreCase) || parts[0].Equals("global", StringComparison.InvariantCultureIgnoreCase))
                {
                    this.scope = parts[0].ToLower();
                    this.Versioned = parts[1];
                }
                else
                {
                    this.Versioned = parts[1];
                }
            }
        }
    }
}

To use this, we might…

AlarmDesignation ad = "global/TPALARM_v3";

This unit test passes as the object is implicitly converted to a string…

[Fact]
public void AlarmDesignation_AndString_AreEqual()
{
    AlarmDesignation x = "TPALARM_v3";
    string y = "TPALARM_v3";
    Assert.Equal(x, y);
}

But we still need the ToString() for something like this…

string val = $"Alarm Designation is: {ad}";

To use in a controller, like this…

[HttpGet("global/alarm-types/{alarmType}/designations")]
public ActionResult<AlarmDesignations> GetGlobalDesignationsForAlarmType(AlarmDesignation alarmType)
{
    return this.GetDesignationsByAlarmType("global", alarmType);
}

…we also need to provide a type converter like this…

using System;
using System.ComponentModel;
using System.Globalization;

public class AlarmDesignationConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value is string strval) return new AlarmDesignation(strval);
        return base.ConvertFrom(context, culture, value);
    }
}
Written on June 24, 2022