63

I'm trying to write an extension method on IEnumerable that will only apply to value types and strings.

public static string MyMethod<T>(this IEnumerable<T> source) where T : struct, string

However 'string' is not a valid constraint as it is a sealed class.

Is there any way to do this?

Edit:

What I'm actually trying to do is prepare a list of values for an "IN" clause in a dynamically constructed SQL.

I have lots of instances of code such as the following that I want to clean up:

sb.AppendLine(string.Format("AND value IN ({0})", string.Join(",", Values.Select(x => x.ToSQL()).ToArray())));

Where ToSQL() has code to handle SqlInjection.

Brett Postin
  • 11,215
  • 10
  • 60
  • 95

5 Answers5

92

Maybe you could restrict to IConvertible types? All the system primitives that can be converted using these interface methods also implement the interface, so this restriction would require T to be one of the following:

  • Boolean
  • Byte
  • Char
  • DateTime
  • Decimal
  • Double
  • Int (16, 32 and 64-bit)
  • SByte
  • Single (float)
  • String
  • UInt (16, 32 and 64-bit)

If you have an IConvertible, chances are VERY good it's one of these types, as the IConvertible interface is such a pain to implement that it's rarely done for third-party types.

The main drawback is that without actually converting T to an instance of one of these types, all your method will know how to do is call the Object and IConvertible methods, or methods that take an Object or IConvertible. If you need something more (like the ability to add and/or concatenate using +), I think that simply setting up two methods, one generic to struct types and a second strongly-typed to strings, would be the best bet overall.

KeithS
  • 70,210
  • 21
  • 112
  • 164
  • 4
    Great idea! I hadn't thought of that. – Don Rolling Mar 11 '13 at 20:02
  • That's brilliant! – Turner Bass Aug 10 '18 at 17:21
  • 1
    This list doesn't contain Nullable types ( – Evgeniy Miroshnichenko Oct 16 '18 at 15:45
  • 1
    @EvgeniyMiroshnichenko It isn't supposed to; the question asks for a type that encompasses value types and strings. Nullables of a value type are not value types and are not strings, so they aren't part of the answer. – KeithS Oct 25 '18 at 15:16
  • @KeithS `Nullable` is a value type (a.k.a. `struct`) – Robert Taylor Oct 21 '22 at 19:24
  • @RobertTaylor - Special case; while you're correct that `Nullable` is defined as a struct, Nullable exists to provide the eponymous behavior of reference types. Your point is taken in that the behavior could be useful when talking about SQL operations, though probably not in the above specific case (`theColumn IN (null)` always returns false even if some fields of `theColumn` are null) – KeithS Oct 24 '22 at 17:02
53

You need to define 2 separate methods:

public static string MyMethod<T>(this IEnumerable<T> source) where T : struct
public static string MyMethod(this IEnumerable<string> source)
Steven
  • 166,672
  • 24
  • 332
  • 435
  • 3
    You could also have a 3rd, private method that both these methods call to keep things a little DRY-er. See [this answer](http://stackoverflow.com/a/4109547/957950) to a similar question. – brichins Jan 25 '17 at 15:55
  • 3
    While the "you can't" answer is more correct, this answer is more useful. – Eric Rini Nov 09 '18 at 14:06
35

No, you can't. Generic constraints are always "AND"-ed, if you see what I mean (i.e. all constraints must be satisifed), so even if you were trying to use some unsealed class, this would still fail.

Why do you want to do this? Perhaps there's another approach which would work better.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Thanks. What would be the best alternative? Two separate methods? – Brett Postin Jan 05 '12 at 16:00
  • 2
    @Poz: Given that I wouldn't format values into SQL to start with, I'd suggest refactoring to use parameterized queries instead... – Jon Skeet Jan 05 '12 at 16:12
  • We initially tried to go down that route. However issues with passing lists as parameters in SQL Server, and the need to split on something that could be valid text within the values made us change our approach. The SQL is also built up dynamically, with conditional joins etc, which we felt would be better done in code rather than within a stored procedure. It is a query that can have many permutations of parameters thrown at it which is why we can't make it static sql. – Brett Postin Jan 05 '12 at 16:27
  • 4
    @Poz: I would suggest dynamically adding enough placeholders into the SQL, but then specifying them as parameter values. Including the values directly is simply too risky, IMO. – Jon Skeet Jan 05 '12 at 16:28
  • Sorry I'm not sure I understand what you mean by dynamically adding placeholders into the sql? The main issue for us is that we have conditional joins based on the filtering parameters passed in, which is why we can't get away from dynamic sql. As an example of one of the issues we encountered using parameters see this [Parameterizing a SQL IN clause?](http://stackoverflow.com/questions/7178548/conversion-failed-when-converting-the-nvarchar-value-to-data-type-int) – Brett Postin Jan 05 '12 at 16:54
  • 3
    @Poz: I mean that if you have two parameters, you create an IN clause of `IN (?, ?)` or `IN(:p1, :p2)` or whatever, and then dynamically add those parameter values to the command in the normal way. Even if the SQL is dynamic, that doesn't mean you have to avoid parameters. There are other alternatives like table-valued parameters too (http://msdn.microsoft.com/en-us/library/bb510489.aspx) depending on what version of SQL server you're using. – Jon Skeet Jan 05 '12 at 17:14
  • Thanks Jon, I understand now from your explanation and the top rated answer in the following question: [Parameter Placeholders](http://stackoverflow.com/questions/337704/parameterizing-a-sql-in-clause). I will also mark your answer as accepted, for the additional help. – Brett Postin Jan 05 '12 at 17:20
21

I used a hack-solution: interface. See the interfaces the built-in value types and string type have implemented:

struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>

class String : IComparable, ICloneable, IConvertible, IComparable<string>, IEnumerable<char>, IEnumerable, IEquatable<string>

struct Boolean : IComparable, IConvertible, IComparable<bool>, IEquatable<bool>

struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable<DateTime>, IEquatable<DateTime>

struct UInt64 : IComparable, IFormattable, IConvertible, IComparable<ulong>, IEquatable<ulong>

struct Single : IComparable, IFormattable, IConvertible, IComparable<float>, IEquatable<float>

struct Byte : IComparable, IFormattable, IConvertible, IComparable<byte>, IEquatable<byte>

struct Char : IComparable, IConvertible, IComparable<char>, IEquatable<char>

struct Decimal : IFormattable, IComparable, IConvertible, IComparable<decimal>, IEquatable<decimal>

You can use IComparable,IConvertible,IEquatable<T>for constraints. Like this:

 public static void SetValue<T>(T value) where T : IComparable, IConvertible, IEquatable<T>
    {
        //TODO:
    }

Or you can use type code to check the data time without constraints.

public static void SetValue<T>(T value)
    {
        switch (Type.GetTypeCode(typeof(T)))
        {
            #region These types are not what u want, comment them to throw ArgumentOutOfRangeException

            case TypeCode.Empty:
                break;
            case TypeCode.Object:
                break;
            case TypeCode.DBNull:

                #endregion

                break;
            case TypeCode.Boolean:
                break;
            case TypeCode.Char:
                break;
            case TypeCode.SByte:
                break;
            case TypeCode.Byte:
                break;
            case TypeCode.Int16:
                break;
            case TypeCode.UInt16:
                break;
            case TypeCode.Int32:
                break;
            case TypeCode.UInt32:
                break;
            case TypeCode.Int64:
                break;
            case TypeCode.UInt64:
                break;
            case TypeCode.Single:
                break;
            case TypeCode.Double:
                break;
            case TypeCode.Decimal:
                break;
            case TypeCode.DateTime:
                break;
            case TypeCode.String:
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

Remember that do not use object type but generic type for parameter type. Otherwise you might get an NULL EXCEPTION at codeline Type.GetTypeCode(value.GetType()) when value is null.

Sean C.
  • 1,494
  • 1
  • 13
  • 22
3

You can use a static constructor to check the type parameter when the class is used.

class Gen<T> {
    static Gen() {
        if (!typeof(T).IsValueType && typeof(T) != typeof(String))
        {
            throw new ArgumentException("T must be a value type or System.String.");
        }
    }
}
Tereza Tomcova
  • 4,928
  • 4
  • 30
  • 29
  • 6
    This doesn't help you at compile time, which is what generics should really be used for. It also is very rude to throw an exception in a constructor, especially in a static constructor - consumers will very likely get an unhelpful "TypeInitializerException" at runtime and have no idea why. – Dan Field Nov 30 '16 at 01:37
  • @DanField If you squint, this can help you at compile time because any closed type referencing `Gen`'s static constructor COULD create a warning that it always throws an exception. Just needs Roslyn supported Static analyzer and it's a complete solution IMHO. The static constructor issue can be avoided by moving the logic to a static readonly field, which will avoid TypeInitializerException. Overall, I think this is fairly ingenius way to encode constraints on primitives. – John Zabroski May 27 '19 at 20:26
  • I don't think that magic existed when I wrote this comment - but if it does now maybe a new answer with how to turn it on and use it would be helfpul. – Dan Field May 28 '19 at 16:23