Generic Type parameters for covariant params array

时光怂恿深爱的人放手 提交于 2019-12-12 03:15:35

问题


I have an interface:

IReadOnlyList<TEntity> Execute<TEntity>(ILambdaQuery<TEntity> query, params ILambdaQuery<TAssociatedEntity>[] associations) where TAssociatedEntity : class, IAggregateRoot;

The interface is used to execute a query in a repository. Where TEntity is the type that you're expecting back and what you are querying and TAssociatedEntity are other queries that you'd like to run and that will be combined with the TEntity results that come back. Eg.

var courseQuery = LambdaQuery<Course>(c => c.Id == id);

var studentsQuery = LambdaQuery<Student>(s => s.Courses.Any(c => c.id == id));

var instructorsQuery = LambdaQuery<Instructor>(i => i.Courses.Any(c => c.id == id));

var courses = this.repo.Execute<Course>(courseQuery, studentsQuery, instructorsQuery);

My question revolves around this parameter:

params ILambdaQuery<TAssociatedEntity>[] associations

So you can see from the example, the associations array could have different types of TAssociatedEntity. So how do I tell the compiler that?

The interface above doesn't do the job. The compiler says that The type of namespace name 'TAssociatedEntity' could not be found...

Is this inpossible? Any ideas out there?

EDIT - a request for more information:

The LambdaQuery class stores a where lambda expression clause and a associated entities to eager fetch. A multitude of these are passed to the repository Execute method which does:

    public IReadOnlyList<TEntity> Execute(ILinqQuery<TEntity> query, params ILinqQuery<TAssociatedEntity>[] associations)
    {
        var queryOver = this.GetSession().QueryOver<TEntity>().Where(query.WhereClause);

        query.FetchExpressions.ToList().ForEach(expression => queryOver = queryOver.Fetch(expression).Eager);

        var future = queryOver.Future<TEntity>();

        foreach (ILinqQuery<TEntity> association in associations)
        {
            var queryOverAssociation = this.GetSession().QueryOver<TEntity>().Where(association.WhereClause);

            association.FetchExpressions.ToList().ForEach(expression => queryOverAssociation = queryOverAssociation.Fetch(expression).Eager);

            queryOverAssociation.Future<TEntity>();
        }

        return this.ExecuteInTransaction(() => future.ToList());
    }

It runs the query and any associated queries as a batch and returns a requested TEntity with any bits filled in.

EDIT - more information

I'm not brilliant with nHibernate. But in my simple terms, anything that is executed with Future() is delayed until the ToList() is called.

So if I had:

class Person
{
   public string Id { get; set; }
   public List<Arm> Arms { get; set; }
   public List<Leg> Legs { get; set; }
}

class Arm
{
   public string Id { get; set; }
   public List<Hand> Hands { get; set; }
}

class Leg
{
   public string Id { get; set; }
   public List<Foot> Feet { get; set; }
}

So if the Execute method was called for a TEntity of Person and a person had a collection of arms, each of which has a hand and a collection of legs, each of which has a foot, then we might have a called to Execute that looks like:

LambdaQuery<Person> personQuery = new LambdaQuery<Person>(t => t.Id == "123");
LambdaQuery<Arm> armQuery = new LambdaQuery<Arm>(t => t.Person.Id == "123", t => t.Hands);

Person person = personRepository.Execute(personQuery, armQuery);

Then it would send the following to the database in batch:

SELECT * FROM person WHERE personId = '123';
SELECT * FROM arms a JOIN hands h ON h.armId = a.armId WHERE a.personId = "123"

And a (legless) Person object would be returned from the Execute method with:

Person
------
{ 
  Id : "123", 
  Arms : [ { Id : "ARM1", Hands : [ { Id : "HAND1" }, { Id : "HAND2" } ] } ], 
  Legs: null 
}

回答1:


Sorry for the long wait, here is what I can tell you.

Preamble

One reason why I have trouble answering is that parts of the question setup don't really seem to fit together, so things might not really work the way you (or I) imagine. And indeed the Execute method you show does not seem to actually do anything with the associations that are passed in, at least nothing that would provide any observable result, as far as I interpret things.

I never used NHibernate, but I do use Hibernate, and I expect that you don't really need to write a query to get associated objects - as long as you set up the associations correctly, the ORM should be able to take care of that for you and either eagerly load the associated entities when you query for a main entity, or lazily load them when you access them.

But even if you need to query for the associated entities manually, I would expect that you need to connect them to your main entities somehow, unless you want to have your Execute method return separate lists of main entities and each associated entity type (but then you might as well get rid of the associations parameter and just query for each list individually). And I don't see anything in your Execute code which connects up the results of the association queries with the main entities (or even retreives the results of those queries).

Unfortunately, it is critical for your actual question about type signatures how exactly you plan to use the query objects, and what you can do with them, and that is still not entirely clear to me.

However, you might still figure out what you need from any attempt at an answer, so I'll just choose one interpretation of how this is all supposed to work and work from that.

Let's get to the point

From your question, Execute will take one query to find the list of "main" entities to return, and additional queries which are used to find associated entities that will be returned as part of the main entity objects. I'll interpret your Execute implementation as an unfinished attempt to make this work. I will also assume that the lists of associated entities you can get through the associations queries contain enough information to figure out which associated entity connects to which main entity, even if that seems far from trivial to me.

So in short, the associations queries would only be used to make several queries for lists of the associated objects, and by some magic process (critically not involving the query objects) the entities in those lists would be connected to the correct main entities.

Your question is how you can pass several associations queries into Execute, which may be queries for different types of entities and thus have different generic type parameters.

First off, lists and arrays can only hold objects of one fixed type. You can put a String and a Timer into the same array, but only if the item type for the array is a common superclass of both - in this case, only an array of object would work.

For your associations parameter, you are trying to pass both LambdaQuery<Student> and LambdaQuery<Instructor> in the same array, so the array's item type would need to be a supertype of both. object always works for this, but is not very useful:

IReadOnlyList<TEntity> Execute<TEntity>(ILambdaQuery<TEntity> query, params object[] associations); // now what?

You can pass in your queries this way, but you will not have much fun trying to use them. What does NOT work, quite importantly, is this:

IReadOnlyList<TEntity> Execute<TEntity>(ILambdaQuery<TEntity> query, params LambdaQuery<object>[] associations);

For that to work, LambdaQuery<T> would have to be covariant, but that would clash with the fact that you can directly access the where clause in the query object which would have to be contravariant if anything. No, the solution does not lie in generic variance here.

If you have control over the LambdaQuery<T> class hierarchy, one solution could be to make another class LambdaQuery (without type parameter!) which LambdaQuery<T> derives from. Then you could pass in a list of LambdaQuery:

IReadOnlyList<TEntity> Execute<TEntity>(ILambdaQuery<TEntity> query, params LambdaQuery[] associations); // Yes!

LambdaQuery would be a common supertype for all LambdaQuery<T>, so this will now compile! However, how do you work with it? This is where things get a little hairy since I don't know how you would connect the entities together, but imagine something like this:

public abstract class LambdaQuery
{
    public abstract void DoAssociationStuff<TMainEntity>(TMainEntity mainEntity, Session session);
}

public abstract class LambdaQuery<TEntity> : LambdaQuery
{
    public void DoAssociationStuff(Session session)
    {
        var queryOverAssociation = session.QueryOver<TEntity>().Where(this.WhereClause);
        this.FetchExpressions.ToList().ForEach(expression => queryOverAssociation = queryOverAssociation.Fetch(expression).Eager);
        queryOverAssociation.Future<TEntity>();
    }
}

public IReadOnlyList<TEntity> Execute(LambdaQuery<TEntity> query, params LambdaQuery[] associations)
{
    var queryOver = this.GetSession().QueryOver<TEntity>().Where(query.WhereClause);
    query.FetchExpressions.ToList().ForEach(expression => queryOver = queryOver.Fetch(expression).Eager);
    var future = queryOver.Future<TEntity>();

    foreach (LambdaQuery association in associations)
    {
        association.DoAssociationStuff(this.GetSession());
    }

    return this.ExecuteInTransaction(() => future.ToList());
}

The idea here is to have a list of things of some common type which does not expose the generic type parameter, but which can pass execution to a subclass method which knows about the generic type parameter.

I hope this helps you, or at least gives some inspiration for finding a solution.

Bonus: Seperate concerns with the visitor pattern

You mentioned you don't want the Execute-specific code to be part of the LamdaQuery class hierarchy. This makes a lot of sense, because it really should not concern the LambdaQuery how it is used, and you don't want to change it or add to it for the next use case that comes along.

Fortunately there is a solution, but as always with these things it requires a bit of indirection. The idea is to implement the visitor pattern for LambdaQuery:

public interface LambdaQueryVisitor
{
    void Visit<TEntity>(LambdaQuery<TEntity> visited);
}

public abstract class LambdaQuery
{
    public abstract void Accept(LambdaQueryVisitor visitor);
}

public class LambdaQuery<TEntity> : LambdaQuery
{
    public override void Accept(LambdaQueryVisitor visitor)
    {
        visitor.Visit(this);
    }
}

This is all the code you need to add to LambdaQuery<T> and its superclass LambdaQuery, and it is not concerned with the specifics of what you want to do. But it enables you to work with collections of LambdaQueries in any context, by implementing a visitor:

public AssociationQueryVisitor : LambdaQueryVisitor
{
    private readonly Session m_session;

    public AssociationQueryVisitor(Session session)
    {
        m_session = session;
    }

    public void Visit<TEntity>(LambdaQuery<TEntity> query)
    {
        var queryOverAssociation = m_session.QueryOver<TEntity>().Where(query.WhereClause);
        query.FetchExpressions.ToList().ForEach(expression => queryOverAssociation = queryOverAssociation.Fetch(expression).Eager);
        queryOverAssociation.Future<TEntity>();
    }
}

public IReadOnlyList<TEntity> Execute(LambdaQuery<TEntity> query, params LambdaQuery[] associations)
{
    var queryOver = this.GetSession().QueryOver<TEntity>().Where(query.WhereClause);
    query.FetchExpressions.ToList().ForEach(expression => queryOver = queryOver.Fetch(expression).Eager);
    var future = queryOver.Future<TEntity>();

    AssociationQueryVisitor visitor = new AssociationQueryVisitor(this.GetSession());

    foreach (LambdaQuery association in associations)
    {
        association.Accept(visitor);
    }

    return this.ExecuteInTransaction(() => future.ToList());
}

Bonus Bonus: dynamic

You should be able to get the same result with less code and without any change to LambdaQuery<T> at all by using dynamic typing. Dynamic typing is a very powerful tool, but opinions differ wildly on how and when it is appropriate to use. Here is how it would look (I hope. This is all from memory and not checked ;))

public void DoAssociationStuff<TEntity>(Session session, LambdaQuery<TEntity> query)
{
    var queryOverAssociation = session.QueryOver<TEntity>().Where(query.WhereClause);
    query.FetchExpressions.ToList().ForEach(expression => queryOverAssociation = queryOverAssociation.Fetch(expression).Eager);
    queryOverAssociation.Future<TEntity>();
}

public IReadOnlyList<TEntity> Execute(LambdaQuery<TEntity> query, params dynamic[] associations)
{
    var queryOver = this.GetSession().QueryOver<TEntity>().Where(query.WhereClause);
    query.FetchExpressions.ToList().ForEach(expression => queryOver = queryOver.Fetch(expression).Eager);
    var future = queryOver.Future<TEntity>();

    foreach (dynamic association in associations)
    {
        DoAssociationStuff(this.GetSession(), association);
    }

    return this.ExecuteInTransaction(() => future.ToList());
}

The downside here is that you could pass anything in that associations array now, and it would only fail at runtime.




回答2:


You havent declared the TAssociatedEntity type as a generic type, this can be done in two ways.

Either modify the Method (which is probably what you want to do)

IReadOnlyList<TEntity> Execute<TEntity,TAssociatedEntity>...

or the Interface if TAssociatedEntity must be covariant.

public interface IDontKnow<in TAssociatedEntity>


来源:https://stackoverflow.com/questions/34340725/generic-type-parameters-for-covariant-params-array

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!