0

So, I've found several answers to half of my issue, but the two pieces I am trying to combine seem to put things together in ways that others haven't run into before (or at least they're not posting about it).

My objective is to be able use my code like this:

    ModelFactory[DataModelX].DataY

What I know I can get is this:

    ModelFactory.Get<DataModelX>.DataY

Admittedly not that much different, but at this point I've felt that I've gotten close and want to see if I can get it.

My current classes are:

public class ModelFactory<T> :  : IReadOnlyList<IDataModel> where T : IDataModel
    private static ModelFactory<IDataModel> _instance;

    private ModelFactory() {}

    public static ModelFactory<IDataModel> ModelFactory => _instance ?? (_instance = new DataModelFactory<IDataModel>());

    private readonly List<IDataModel> _models = new List<IDataModel>();

    private T GetHelper(Type type)
    {
        // check if model exists
        foreach (var model in _models.Where(model => model.GetType() == type))
        {
            // model exists, return it
            return (T) model;
        }
        // model doesn't exist, so we need to create it
        var newModel = CreateModel(type);
        _models.Add(newModel);
        return newModel;
    }

    public T this[
        Type type
    ] {
        get
        {
            return GetHelper(type);
        }
    }

And DataModels like so:

public class DataModel1 : IDataModel
    public string Alpha {get; set;}
    public int Beta {get; set;}
    // etc...

public class DataModel2 : IDataModel
    public foo Cat {get; set;}
    public foobar Dog {get; set;}
    // etc...

And so on.

However, when I try to type ModelFactory.ModelFactory[DataModelX]. I can't get access to ModelFactory. The property is obviously public, and contained within the appropriate class. Also, I can't pass the class as a parameter (of course), but I don't know how to adjust the parameter on the array indexing to accept the class as a parameter.

Constaints:

  1. ModelFactory should be a static class or a singleton.
  2. DataModels are classes that all implement IDataModel.
  3. Data properties are unique to each individual DataModel
JuniorIncanter
  • 1,569
  • 2
  • 16
  • 27

2 Answers2

2

You can use typeof(T) in order to get the type description of the generic parameter T.

T newModel = (T)CreateModel(typeof(T));

There is no need to have an additional Type parameter, if it always describes the same type as T.

Also you could use a dictionary instead of a list for storing models.

private readonly Dictionary<Type,IDataModel> _models = new Dictionary<Type,IDataModel>();

Then you can get a value with

IDataModel model;
If (!_models.TryGetValue(typeof(T), out model)) {
    model = CreateModel(typeof(T));
    _models.Add(typeof(T), model);
}
return (T)model;

The generic type parameter of your class is misleading as it seems to imply that you are going to create one factory per model type. Just drop it.

Also you cannot implement IReadOnlyList<out T> as it requires the index to be an int. Drop it.

public class ModelFactory { ... }

Now you have two options:

  1. Let the indexer or an ordinary Get-method return a IDataModel and later cast the result

    var m = (MyModelType)factory.Get(typeof(MyModelType));.
    or
    var m = (MyModelType)factory[typeof(MyModelType)];.

  2. Declare a get method with a generic type argument (but no generic type argument for the class!). Indexers cannot have generic type arguments.

public T Get<T>() : where T : IDataModel
{
    ... (same as above)
    return (T)model;
}

call it with

var m = factory.Get<MyModelType>();
Community
  • 1
  • 1
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • My assumption is that won't create the correct DataModel. I need this to be able to handle multiple different DataModels simultaneously. Or am I mistaken? – JuniorIncanter May 19 '16 at 16:31
  • Why does the class `ModelFactory` have a generic type parameter `T`? This leads to the assumption that one factory will be created for each model type. I think the problem is that `IReadOnlyList.this[]` expects an `int` argument, not a `Type`. – Olivier Jacot-Descombes May 19 '16 at 16:35
  • Solely because I want to return a generic type (that implements IDataModel) with `IReadOnlyList.this[]` In other words, I want to have multiple data models, and be able to access them like this: `ModelFactory[DataModelX].DataY` This is my end goal, and I am trying to figure out what implementation will allow this, if it exists. – JuniorIncanter May 19 '16 at 16:38
  • 1
    If you declare a generic type argument for the class, you will need to create an object with `new ModelFactory()`. And the whole factory will be tied to this one type. Since you don't know the model type when creating the factory, but instead want to return different types of models from the same factory, use a generic type argument for the `Get` method only. – Olivier Jacot-Descombes May 19 '16 at 16:55
  • Okay, thanks. So the short answer to my question is 'no'. – JuniorIncanter May 19 '16 at 17:24
  • #2 is the option that I had before, but I was wondering if I could use the array notation [] – JuniorIncanter May 19 '16 at 17:25
  • Indexers (`this[]`) and properties cannot be generic (but they can use the generic type parameters of the class of course, which is of no use here). – Olivier Jacot-Descombes May 19 '16 at 20:03
  • Which is why I was investigating them. Ah well. Thanks anyways. – JuniorIncanter May 19 '16 at 21:13
1

While it would be nice to have, what you are asking for is unfortunately impossible.

There are three possible ways of implementing this factory, none of which do what you want:

1) The implementation you have, which is a generic ModelFactory<T> where T : IDataModel. As has been mentioned, a ModelFactory<T> would force you to limit your factory to take a single type - as in, you would have to define a ModelFactory<DataModel1>, a ModelFactory<DataModel2>, etc. That's a questionable design decision anyway, but more to the point, you can only ever put in and get out the concrete IDataModel type you used to create the factory. For example, you can't create a ModelFactory<DataModel1> and get a DataModel2 from the indexer. So there's not much point to using this implementation.

2) A non-generic ModelFactory with an indexer that returns an IDataModel. This is perfectly valid, but you would have to cast the output to get at the concrete type. So your call syntax using the indexer would be ((DataModel2)ModelFactory[DataModel2]).Cat, or (ModelFactory[DataModel2] as DataModel2).Cat. In my opinion, this is a LOT uglier than a generic Get<T>() function.

3) A non-generic ModelFactory with a generic indexer, such as public T this<T>[Type type] where T : IDataModel. This is closest to what you want, but unfortunately, it is explicitly disallowed under current (C# 6) rules. See, for example, Why it is not possible to define generic indexers in .NET?. It's worth noting, however, that you still wouldn't be able to get the compiler to automatically recognize type as T. You couldn't write public T this<T>[T type] because that would interpret type as an object of type T, nor could you write public T this<T>[typeof(T) type] or something like it, since typeof(T) is an object of type Type. So you would have to call your indexer with ModelFactory<DataModel2>[typeof(DataModel2)].Cat (still ugly!), and implement a runtime check inside to ensure that type is really type T.

So the cleanest option you have is to use a non-generic ModelFactory that retrieves IDataModel implementations from an internal Dictionary<Type, IDataModel>, using public T Get<T>() where T : IDataModel { ... }.

Community
  • 1
  • 1
Diosjenin
  • 733
  • 3
  • 8
  • Yeah, this is what I was afraid of. I was hoping that there was just something clever I was missing. I had gotten myself to use `public T Get() where T` and was hoping I could make that last jump. – JuniorIncanter May 19 '16 at 18:30