This is the thirtieth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
TypeScript has a very nice feature called conditional types. Can we mimic something like this in C#? Let’s say that we have one API endpoint that should return different output depending on the type of the input.
We can start with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
using System; public class Program { public static void Main() { Console.WriteLine(GetForInput(new ScenarioAInput()).GetType()); Console.WriteLine(GetForInput(new ScenarioBInput()).GetType()); } public static Output<T> GetForInput<T>(Input<T> input) where T: Scenario{ if(typeof(Input<ScenarioA>).IsAssignableFrom(input.GetType())){ return (Output<T>)(object)GetForInputA((ScenarioAInput)(object)input); } if(typeof(Input<ScenarioB>).IsAssignableFrom(input.GetType())){ return (Output<T>)(object)GetForInputB((ScenarioBInput)(object)input); } throw new Exception("Bang!"); } public static ScenarioAOutput GetForInputA(ScenarioAInput input){ return new ScenarioAOutput(); } public static ScenarioBOutput GetForInputB(ScenarioBInput input){ return new ScenarioBOutput(); } } public abstract class Scenario { } public class ScenarioA : Scenario{ } public class ScenarioB: Scenario{ } public abstract class Input<T> where T: Scenario{ } public class ScenarioAInput : Input<ScenarioA> { } public class ScenarioBInput : Input<ScenarioB> { } public abstract class Output<T> where T: Scenario{ } public class ScenarioAOutput: Output<ScenarioA> { } public class ScenarioBOutput: Output<ScenarioB>{ } |
This works but is a little bit cumbersome. When we need to add new implementation, we need to add another if condition. Can we do better? Well, a little:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
using System; using System.Linq; using System.Collections.Generic; public class Program { public static void Main() { Console.WriteLine(GetForInput(new ScenarioAInput()).GetType()); Console.WriteLine(GetForInput(new ScenarioBInput()).GetType()); } public static Output<T> GetForInput<T>(Input<T> input) where T: Scenario<T> { List<Scenario> implementations = new List<Scenario>(){ new ScenarioA(), new ScenarioB() }; foreach(var implementation in implementations){ if(implementation.GetType().GetInterfaces().First().GetGenericArguments().First() == typeof(T)){ return ((Scenario<T>)implementation).Calculate(input); } } throw new Exception("Didn't work"); } } public interface Scenario{ } public interface Scenario<T> : Scenario where T: Scenario<T> { Output<T> Calculate(Input<T> input); } public class ScenarioA : Scenario<ScenarioA> { public Output<ScenarioA> Calculate(Input<ScenarioA> input){ return new ScenarioAOutput(); } } public class ScenarioB: Scenario<ScenarioB> { public Output<ScenarioB> Calculate(Input<ScenarioB> input){ return new ScenarioBOutput(); } } public abstract class Input<T> where T: Scenario<T> { } public class ScenarioAInput : Input<ScenarioA> { } public class ScenarioBInput : Input<ScenarioB> { } public abstract class Output<T> where T: Scenario<T> { } public class ScenarioAOutput: Output<ScenarioA> { } public class ScenarioBOutput: Output<ScenarioB> { } |
Now adding new type requires adding new instance to the collection. However, this time we can scan all the types and crate them on the fly as needed with DI or whatever other mechanism.