C# classes for system health metrics My company wants me to build a serverless component (AWS Lambda) that checks every minute for our system components health.

The data model (classes structure) I have devised so far is as pasted below.
The main problem I am trying to address is I wanted to have a collection of all our system metrics. At first I thought of having clsMetric&lt;T&gt; where T would determine the type of Value and Threshold properties. But I can't have a collection of a generic class (at least not a generic collection of generic classes).

So I ended up creating subclasses of clsMetric for each metric value type I have, and left in the parent class a string representation of those properties for accessing them while looping over my collection of clsMetric.

Is this the way to go ? Current C# capabilities offer something else I do not know? Any design pattern suitable to apply here I am not aware of ?

public abstract class clsMetric
{
    public string Name { get; }
    public abstract string ValueAsStr { get; }
    public abstract eLogicalOperator LogicalOperator { get; }
    public abstract string ThresholdAsStr { get; }
    public abstract bool Faulty();
    public clsMetric(string name)
    {
         Name = name ?? string.Empty;
    }

    public override string ToString()
    {
        return Name;
    }
}


public class IntegerMetric : clsMetric
{
    eLogicalOperator _logicalOperator;
    #region Properties
    public int Value { get; private set; }

    public override string ValueAsStr =&gt; Value.ToString();

    public override eLogicalOperator LogicalOperator =&gt; _logicalOperator;

    public int Threshold { get; private set; }
    public override string ThresholdAsStr =&gt; Threshold.ToString();

    #endregion

    #region Constructors
    public IntegerMetric(string name, int value, eLogicalOperator logicalOperator, int threshold) : base(name)
    {
        Value = value;
        _logicalOperator = logicalOperator;
        Threshold = threshold;
    }

    public override bool Faulty()
    {
        if (LogicalOperator == eLogicalOperator.Equal)
            return Value == Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterOrEqualThan)
            return Value &gt;= Threshold;
        else if (LogicalOperator == eLogicalOperator.GreaterThan)
            return Value &gt; Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerOrEqualThan)
            return Value &lt;= Threshold;
        else if (LogicalOperator == eLogicalOperator.LowerThan)
            return Value &lt; Threshold;

        return false;
    }

    #endregion
}


public class StringMetric : clsMetric
{
    eLogicalOperator _logicalOperator;
    #region Properties
    public string Value { get; private set; }

    public override string ValueAsStr =&gt; Value.ToString();
    public override eLogicalOperator LogicalOperator =&gt; _logicalOperator;

    public string Threshold { get; private set; }

    public override string ThresholdAsStr =&gt; Threshold.ToString();
    #endregion

    #region Constructors
    public StringMetric(string name, string value, eLogicalOperator logicalOperator, string threshold) : base(name)
    {
        Value = value;
        _logicalOperator = logicalOperator;
        Threshold = threshold;
    }

    public override bool Faulty()
    {
        if (LogicalOperator == eLogicalOperator.Equal)
            return string.Equals(Value, Threshold);
        else if (LogicalOperator == eLogicalOperator.Contains)
            return ValueAsStr.Contains(ThresholdAsStr, StringComparison.InvariantCultureIgnoreCase);

        return false;
    }
    #endregion
}
A design pattern often used, is to have a non generic base type and a generic type derived from it or a type implementing a non generic as well as a generic interface. An example is

public abstract class Comparer&lt;T&gt; :
    System.Collections.Generic.IComparer&lt;T&gt;, System.Collections.IComparer


You can see the full implementation here public abstract class Comparer&lt;T&gt; : IComparer, IComparer&lt;T&gt;.

You can see that it does an explicit implementaion of IComparer members and an implicit implementation of IComparer&lt;T&gt;. This hides the IComparer members when not accessed through this very interface. Original code:

public abstract int Compare(T x, T y);

int IComparer.Compare(object x, object y) {
    if (x == null) return y == null ? 0 : -1;
    if (y == null) return 1;
    if (x is T &amp;&amp; y is T) return Compare((T)x, (T)y);
    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArgumentForComparison);
    return 0;
}


A possible approach for your metric class, would be to implement two interfaces IMetric and IMetric&lt;T&gt;. The collection would then have an element type of IMetric.

public interface IMetric
{
    string Name { get; }
    object Value { get; }
    eLogicalOperator LogicalOperator { get; }
    object Threshold { get; }
    bool Faulty();
}

public interface IMetric&lt;out T&gt; : IMetric
{
    new T Value { get; }
    new T Threshold { get; }
}


Implementation:

public class Metric&lt;T&gt; : IMetric&lt;T&gt;
{
    public Metric(string name, eLogicalOperator logicalOperator, T value, T threshold)
    {
        Name = name ?? String.Empty;
        LogicalOperator = logicalOperator;
        Value = value;
        Threshold = threshold;
    }

    public T Value  { get; }
    public T Threshold  { get; }

    object IMetric.Value =&gt; Value;
    object IMetric.Threshold =&gt; Threshold;

    public string Name  { get; }
    public eLogicalOperator LogicalOperator { get; }

    public bool Faulty()
    {
        //TODO: implement;
    }
}


I don't know, however, how you would implement the Faulty method in a generic way. Maybe you could inject the logic and use a factory method to create metric objects.

Probably a better approach is to create an abstract Metric&lt;T&gt; class having only the Faulty method abstract to be able to implement specialized variants.

public class DecimalMetric : IMetric&lt;decimal&gt;


Yet another, elegant possibility is to pass a delegate doing the comparison, instead of the operator enum.

public Metric(string name, T value, T threshold, Func&lt;T,T,bool&gt; isValid)


This allows you to provide a type specific comparison and saves you a complex processing for different operators. I prefer to ask the positive question "is valid". It feels better than specifying what is wrong. You can call it like this

new Metric&lt;int&gt;("int test", 5, 10, (v, threshold) =&gt; v &lt; threshold)


Usage (still with you original constructor):

var list = new List&lt;IMetric&gt;();
list.Add(new Metric&lt;int&gt;("int", eLogicalOperator.LowerThan, 5, 10));
list.Add(new Metric&lt;double&gt;("double", eLogicalOperator.LowerThan, 2.45, 10.0));

object value = list[0].Value;

switch (list[0]) {
    case Metric&lt;int&gt; intMetric:
        int i = intMetric.Value;
        break;
    case Metric&lt;double&gt; doubleMetric:
        double d = doubleMetric.Value;
        break;
    default:
        break;
}

// or

switch (list[0].Value) { // of type object
    case int i:
        int myInt = i;
        break;
    case double d:
        double myDouble = d;
        break;
    default:
        break;
}