5earch
5earch

Reputation: 289

How to implement generic method with constraints

from my title it might be a little hard to understand what I'm trying to achieve, so I'll go a little further into detail.

I have the following interface:

public interface IModelBuilder<T>
    where T : IStandardTemplateTemplate
{
    M Build<M>(T pTemplate, params object[] pParams) where M : BaseModel;
}

Now I want to implement the interface in my actual builder. The builder I use to map different object types. So this looks as follows:

public class BusinessModelBuilder : IModelBuilder<IBusinessTemplate>
{
    public virtual M Build<M>(IBusinessTemplate pTemplate, params object[] pParams) where M : BussinessModel
    {
        var businessModel = Activator.CreateInstance<M>();

        // map data

        return businessModel;
    }
}

Now the problem is the following. I can't get the constraint to work. Since I defined the constraint on the interface it won't let me use a different constraint on my actual method, even though my BusinessModel inherits from BaseModel. It keeps telling me my constraint M must match the constraint from the interface. I tried several different approaches, but none seem to work.

Does anyone know if or how this can be achieved? I just want to tell my constraint in the interface that inherited models are allowed.

Upvotes: 5

Views: 1248

Answers (4)

Servy
Servy

Reputation: 203802

Here's a short but complete example of your problem:

public class Parent { }
public class Child { }

public interface Interface
{
    void Foo<T>() where T : Parent;
}

public class Implementation : Interface
{
    public void Foo<T>() where T : Child
    {

    }
}

This won't compile, for the same reason that yours won't. The implementation of the interface method must have the exact same constraint on the generic argument, it can't have a more restrictive constraint.

Your Build method can only constrain the type to BaseModel, not BussinessModel.

If you alter your IModelBuilder interface to add an additional class level generic argument, and then use that as the constraint, then you can get the desired functionality:

public interface IModelBuilder<T, Model>
    where T : IStandardTemplateTemplate
    where Model : BaseModel
{
    M Build<M>(T pTemplate, params object[] pParams) where M : Model;
}    

public class BusinessModelBuilder : IModelBuilder<IBusinessTemplate, BussinessModel>
{
    public virtual M Build<M>(IBusinessTemplate pTemplate, params object[] pParams)
        where M : BussinessModel
    {
        var businessModel = Activator.CreateInstance<M>();

        // map data

        return businessModel;
    }
}

This solution is based in part on Reed's answer but which takes it a step further.

Upvotes: 5

Tejs
Tejs

Reputation: 41236

This is because the constraint is actually different, so it does not satisfy the interface.

IModelBuilder<IBusinessTemplate> sample = new BusinessModelBuilder();
sample.Build(??) 

The compiler would be expecting the arguments to Build to be a BaseModel, but since you can have a type inheriting from BaseModel that is not a BusinessModel, this is obviously invalid.

Now, to get to the actual meat of the problem, you can solve this with another generic argument.

 public interface IModelBuilder<TemplateType, ConstraintType>
 {
     public ConstraintType Build<ConstraintType>(TemplateType template, params object[] parameters);
 }

Alternatively, you can switch things around and get some generic type inference if you really want to go crazy (assuming this is even applicable to your argument):

public interface IStandardTemplate<TModel> { }

public interface IModelBinder<TModel>
{
     TModel ApplyParameters(IStandardTemplate<TModel> template, params object[] parameters);
}

public class ModelBuilder
{
      public TModel Build<TModel>(IStandardTemplate<TModel> template, params object[] parameters)
      {
          var model = Activator.CreateInstance<TModel>();

          var modelBinder = ModelBinderFactory.CreateBinderFor(model);

          return modelBinder.ApplyParameters(template, parameters);
      }
}

Then you can simply make various ModelBinder classes and associate them in your factory.

Example call:

  public class BusinessTemplate : IStandardTemplate<BusinessModel> { }

  var businessTemplate = new BusinessTemplate();
  var model = new ModelBinder().Build(businessTemplate); // model is of type 'BusinessModel'

Upvotes: 0

nhrobin
nhrobin

Reputation: 903

You can define your interface little bit differently to include e.g.

public interface IModelBuilder<T,M>
    where T : IStandardTemplateTemplate
    where M: BaseModel
{
    M Build<M>(T pTemplate, params object[] pParams);
}

and use it like below

public class BusinessModelBuilder : IModelBuilder<IBusinessTemplate,BusinessModelBuilder>

Upvotes: 0

Reed Copsey
Reed Copsey

Reputation: 564333

You'll need to put both in as class constraints:

public interface IModelBuilder<T, M>
    where T : IStandardTemplateTemplate
    where M : BaseModel
{
    M Build<M>(T pTemplate, params object[] pParams);
}

You can then use:

public class BusinessModelBuilder : IModelBuilder<IBusinessTemplate, BusinessModel>
{
   public virtual BusinessModel Build(IBusinessTemplate pTemplate, params object[] pParams)
   {
       //...

Upvotes: 2

Related Questions