How to map a default LINQ expression in a generic

2019-05-18 01:17发布

问题:

I have a generic Repo class that exposes a database LINQ provider:

class Repo<T> where T : IPersisted
{
    IQueryable<T> Get()
    {
        return _queryable;
    }
}

(IPersisted is a simple marker interface for persisted objects).

Now... I would like to find an elegant way to inject a default LINQ expression for certain derived types of IPersisted. For example, for the following IPersisted implementation:

class Deletable : IPersisted
{
    bool IsDeleted { get; set; }
}

I want the IQueryable<T> Get() method to return _queryable.Where(q => !q.IsDeleted), only when T is of type Deletable.

I thought about creating some type of dictionary map IDictionary<Type, Expression>, and doing a lookup on typeof(T) inside the Get method but I'm not sure that I'll be able to strongly type the expressions in this case.

How can I inject a "default" LINQ expression into the Get method, based on the type of T? I'd like to have an extensible, object oriented way of mapping types to default expressions.

回答1:

Since each IPersisted type is going to have its own expression, I would make that a constraint on the part of IPersisted. So now you have some kind of polymorphism.

Something like?

interface IPersisted<T> where T: IPersisted<T>
{
    Expression<Func<T, bool>> Predicate { get; }
}

class Repo<T> where T : IPersisted<T>, new()
{
    public IQueryable<T> Get()
    {
        var dummy = new T();
        return _queryable.Where(dummy.Predicate);
    }
}

class Deletable : IPersisted<Deletable>
{
    public Deletable()
    {

    }

    public Expression<Func<Deletable, bool>> Predicate
    {
        get { return x => !x.IsDeleted; }
    }

    bool IsDeleted { get; set; }
}

I think what you need here is some kind of static polymorphism, but since C# doesnt offer that, you might need create a dummy instance for yourself, just to get the expression.


If you can't have a default constructor, then you can rely on FormatterServices.GetUninitializedObject(t). You can adjust your Get method like this:

public IQueryable<T> Get()
{
    var dummy = (T)FormatterServices.GetUninitializedObject(typeof(T));
    return _queryable.Where(dummy.Predicate);
}

Two things out of possibly many things to note about FormatterServices.GetUninitializedObject:

  1. It will not initialize anything or run the constructor, but that shouldn't be a problem for us.

  2. It's relatively slower. Shouldn't be a big deal since you can cache the instances :) Something like:

    class Repo<T> where T : IPersisted<T>
    {
        //caching mechanism: this is run only once per instance; you can make it 
        //static if this shud be run only once the entire lifetime of application
        readonly T dummy = (T)FormatterServices.GetUninitializedObject(typeof(T));
    
        public IQueryable<T> Get()
        {
            return _queryable.Where(dummy.Predicate);
        }
    }
    

If the exact expression is not important, then you can get rid of the object instantiation. Something like:

interface IPersisted<T> where T: IPersisted<T>
{
    Func<T, bool> Predicate { get; }
}

class Repo<T> where T : IPersisted<T>
{
    public IQueryable<T> Get()
    {
        return _queryable.Where(x => x.Predicate(x));
    }
}

class Deletable : IPersisted<Deletable>
{
    public Func<Deletable, bool> Predicate
    {
        get { return x => !x.IsDeleted; }
    }
}

If preserving the original definition of IPersisted is important, then you can make it non-generic. Not sure if that would make it any less strongly-typed.

interface IPersisted
{
    Expression<Func<object, bool>> Predicate { get; }
}

class Repo<T> where T : IPersisted
{
    public IQueryable<T> Get()
    {
        return _queryable.Where(dummy.Predicate);
    }
}

class Deletable : IPersisted
{
    public Expression<Func<object, bool>> Predicate
    {
        get { return x => !((Deletable)x).IsDeleted; }
    }
}

The above approach can be made more strongly typed by going for a method in IPersisted but need not be good enough a constraint:

interface IPersisted
{
    Expression<Func<T, bool>> GetPredicate<T>() where T : IPersisted;
}

class Repo<T> where T : IPersisted
{
    public IQueryable<T> Get()
    {
        return _queryable.Where(dummy.GetPredicate<T>());
    }
}

class Deletable : IPersisted
{
    Expression<Func<T, bool>> IPersisted.GetPredicate<T>() //made it explicit
    {
        return x => ((Deletable)(object)x).IsDeleted;
    }
}

Note: Make the Predicate implementation explicit if it doesn't make sense outside the Repo<T> class.



回答2:

Not sure if we're 100% on the same page but this get method example will take a linq Func which you can use as a where clause to filter your query.

class Repo<T> where T : IPersisted
{
    IQueryable<T> Get(Func<T, bool> filter)
    {
        return _queryable.Where(filter);
    }
}

Edit: Not sure if this will translate well to sql but to make sure i have the right idea of what you want (and maybe spark a light) : Have you considered something like this?

class Repo<T> where T : IPersisted
{
    IQueryable<T> Get()
    {
        if (typeof (T).IsAssignableFrom(typeof (IDeletable)))
        {
            return _queryable.Where(o => ((IDeletable) o).Deleted = false).AsQueryable();
        }
        return _queryable;
    }
}