No, the split happens once.
states.Split(',') returns an array. Array's in .NET implement IEnumerable
In general .NET collections are either vectors, arrays or other collections implementing IEnumerable or providing a GetEnumerator() method that returns an enumerator object with a property Current and a method MoveNext(). In some cases, the compiler will generate code to use GetEnumerator(), in other cases it will emit simple vector instructions using ldelem.ref, in other words, converting the foreach to a for loop.
At the start of the foreach() statement, the topic of the iteration, states.Split(), will be evaluated exactly once. In C#, at compile time it is decided what sort of container we are iterating, and choose a strategy. The compiler generates code to return the array (or other enumerable result) into a temporary variable, then the loop proceeds to access the N-th item from array one by one. Once the scope is destroyed, the "temp" container is garbage collected.
Now the compiler doesn't always use IEnumerator. It may convert a foreach() into a for() loop.
Consider:
string states = "1,2,3";
foreach (var state in states.Split(','))
{
Console.WriteLine(state);
}
Sample MSIL:
IL_0017: ldloc.s CS$0$0000
IL_0019: callvirt instance string[] [mscorlib]System.String::Split(char[]) // happens once
IL_001e: stloc.s CS$6$0001 // <--- Here is where the temp array is stored, in CS$6$0001
IL_0020: ldc.i4.0
IL_0021: stloc.s CS$7$0002 // load 0 into index
IL_0023: br.s IL_003a
IL_0025: ldloc.s CS$6$0001 // REPEAT - This is the top of the loop, note the Split is above this
IL_0027: ldloc.s CS$7$0002 // index iterator (like (for(int i = 0; i < array.Length; i++)
IL_0029: ldelem.ref // load the i-th element
IL_002a: stloc.1
IL_002b: nop
IL_002c: ldloc.1
IL_002d: call void [mscorlib]System.Console::WriteLine(string)
IL_0032: nop
IL_0033: nop
IL_0034: ldloc.s CS$7$0002
IL_0036: ldc.i4.1 // add 1 into index
IL_0037: add
IL_0038: stloc.s CS$7$0002
IL_003a: ldloc.s CS$7$0002
IL_003c: ldloc.s CS$6$0001
IL_003e: ldlen
IL_003f: conv.i4
IL_0040: clt // compare i to array.Length
IL_0042: stloc.s CS$4$0003 // if i < array.Length
IL_0044: ldloc.s CS$4$0003 // then
IL_0046: brtrue.s IL_0025 // goto REPEAT (0025) for next iteration