While I think making the code table driven is a reasonable idea, I think you're doing that (mostly) the wrong way.
If you're going to go beyond the obvious solution (e.g., @DanienMeSer's), you should accomplish something by doing so. The obvious problems (IMO) with the original code are that it's not very extensible (e.g., if you had 50 cases instead of three, you'd probably need to rewrite entirely), and it doesn't separate concerns very well (e.g., the logic and the I/O are quite tightly intermingled).
At least IMO, the key here is ensuring that each function (including main) has a clearly defined purpose and layer of abstraction at which it operates. If it delegates everything to lower layers, then it's not doing anything to justify its own existence. At the opposite extreme, doing everything in one layer/function means you're not actually using functions, classes, etc., to keep the code simple and manageable.
With that idea in mind, I'd start by thinking about what the ideal top-level code would look like, then write the lower layers to support that. In this case, it seems to me that it makes sense for the top layer to generate inputs, and write out the result for each:
for (int i=start; i<end; i++)
console.WriteLine(fizzbuzz(i));
That's pretty simple, but it still accomplishes something useful: it generates inputs, writes outputs, and isolates logic from I/O. This is a decided contrast to a Main that just contains a single function call to doEverything(); (usually under some other name) that just forces the reader to navigate through more code before they find anything meaningful.
From there, we obviously need to define the fizzbuzz function to do the dirty work:
string fizzbuzz(int i) {
// logic here
}
We have two choices for implementing that. One is mostly monolithic, but still make use of your table to produce the values:
string fizzbuzz(int i) {
string ret;
foreach (var comb in combinations) {
if (i % comb.Item1 == 0)
ret += comb.item2;
}
if (ret.Length() == 0)
ret = i.toString();
return ret;
}
Personally, I think I'd break that up into two pieces though: one that's a simple map from multiple of 3/5 to "fizz"/"buzz", and the other to provide a default value in case the first "fails":
string check_mult(int i) {
string ret = new String();
foreach (var comb in combinations) {
if (i % comb.Item1 == 0)
ret += comb.item2;
}
return ret;
}
...then the second, with the full fizzbuzz logic, which is now pretty trivial:
string fizzbuzz(int i) {
string ret = check_mult(i);
if (ret.length() == 0)
ret = i.toString();
return ret;
}
I'd note that these also give us some building blocks that at least at first blush look like they stand at least a little chance of being useful. One maps multiples of arbitrary numbers to arbitrary strings, and another attempts to map numbers to strings, with a default of mapping the input directly to a string if the first fails.
If you wanted to make this a bit more generic, you'd put just a bit more of the logic into the table, so the first element in the tuple was an object instead of just a value. Then instead of dealing only with multiples of specified values, you could deal with an arbitrary function of an input value. I'll leave that modification for somebody else to deal with though.