Ravi V
Ravi V

Reputation: 101

How To run LINQ queries with Generic type

This is My Class which is basically Closed Coupled To Type I want to Generalise this Class.

So I can use with all the Class

one thing is common that there will be a Name field while invoking this method

    public class CustomSuggestionProvider : ISuggestionProvider
    {
        private const int batchSize = 30;
        private string _criteria = string.Empty;
        private int _skipCount;

        private readonly ObservableCollection<LanguageItem> _observableCollection;
        private readonly List<LanguageItem> _source;

        public CustomSuggestionProvider(ObservableCollection<LanguageItem> observableCollection, List<LanguageItem> source)
        {
            _observableCollection = observableCollection;
            _source = source;
        }

        public bool HasMoreSuggestions { get; private set; } = true;

        public Task<IList<object>> GetSuggestionsAsync(string criteria, CancellationToken cancellationToken)
        {
            _criteria = criteria;
            var newItems = _source.Where(x => x.Name.IndexOf(_criteria, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
            if (cancellationToken.IsCancellationRequested)
                return null;
            HasMoreSuggestions = newItems.Count > batchSize;
            _skipCount = batchSize;
            return Task.FromResult<IList<object>>(newItems.Take(batchSize).Cast<object>().ToList());
        }

     
    }

I Just made it Generalised like this.

Made all the closely coupled class to Type T.

Kindly have a look.

 public class CustomSuggestionProvider<T> :  ISuggestionProvider
    {
        private const int batchSize = 30;
        private string _criteria = string.Empty;
        private int _skipCount;
        
        private readonly ObservableCollection<T> _observableCollection;
        private readonly List<T> _source;

        public CustomSuggestionProvider(ObservableCollection<T> observableCollection, List<T> source)
        {
            _observableCollection = observableCollection;
            _source = source;
        }
 

        public bool HasMoreSuggestions { get; private set; } = true;
]


        public Task<IList<object>> GetSuggestionsAsync(string criteria, CancellationToken cancellationToken)
        {
            _criteria = criteria;
                var newItems = _source.Where(x =>Name.IndexOf(_criteria, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
            if (cancellationToken.IsCancellationRequested)
                return null;
            HasMoreSuggestions = newItems.Count > batchSize;
            _skipCount = batchSize;
            return Task.FromResult<IList<object>>(newItems.Take(batchSize).Cast<object>().ToList());
        }

        public Task<IList<object>> GetSuggestionsAsync(CancellationToken cancellationToken)
        {
            var newItems = _source.Where(x => x.Name.StartsWith(_criteria)).Skip(_skipCount).ToList();
            if (cancellationToken.IsCancellationRequested)
                return null;
            HasMoreSuggestions = newItems.Count > batchSize;
            _skipCount += batchSize;
            return Task.FromResult<IList<object>>(newItems.Take(batchSize).Where(x => !_observableCollection.Any(y => y.Id == x.Id)).Cast<object>().ToList());
        }
    }

I just stuck when at this statement

The 'Name' does not exist here

var newItems = _source.Where(x => x.Name.IndexOf(_criteria, StringComparison.OrdinalIgnoreCase) >= 0).ToList();

Can you please help troubleshoot this.

Thanks in Advance.

Upvotes: 1

Views: 91

Answers (3)

TheGeneral
TheGeneral

Reputation: 81523

Assuming every type you want to use has a Name property, you would just need an interface and a constraint:

Constraints inform the compiler about the capabilities a type argument must have. Without any constraints, the type argument could be any type. The compiler can only assume the members of System.Object, which is the ultimate base class for any .NET type. For more information, see Why use constraints. If client code uses a type that doesn't satisfy a constraint, the compiler issues an error. Constraints are specified by using the where contextual keyword

Given:

public interface IName
{
    public string Name { get; set; }
}

You would put this interface on every type you want to use as the generic parameter to your class:

public class Bob : IName
{
   ...
}

Example

Then just add the constraint to the class with where:

public class CustomSuggestionProvider<T> :  ISuggestionProvider 
    where T: IName
{

}

Note there are other options if you can't constrain the generic type, either using Func<T,bool> or an expression depending on the use case.

Upvotes: 5

Harald Coppoolse
Harald Coppoolse

Reputation: 30474

There are several methods that you could use. Which one you use depends on how generic you want your class to be.

Promise to implement an interface with property Name

var newItems = _source.Where(x => x.Name...

Here every x is an object of Type T. Apparently you plan to use your generic class only for T classes that have a property Name. You'll have to promise the compiler that you will only use T that have this property.

interface IHasName
{
    string Name {get; }
}

public class CustomSuggestionProvider<T> :  ISuggestionProvider
       where T : IHasName
{
     ...

Usage:

class MyClass : IHasName {...}

var provider = new CustomSuggestionProvider<MyClass>(...);

The disadvantage is that every class that you want to use for your CustomSuggestionProvider has to implement this interface.

Tell your CustomSuggestionProvider which property to use

Another Disadvantage of the interface method is that it only works for property Name. Suppose you want this for a class with a LastName, or maybe with an integer Id?

You want to use the Name in a Where. You select a string property from your T, and compare it with string _criteria. Why not tell your class which property it should select? To find out how to select a property, see Enumerable.Select

class CustomSuggestionProvider<T> : ...
{
     public Func<TSource,string> ItemSelector {get; set;} t => t.ToString();

     ...

     private IEnumerable<T> NewItems => this._source
         .Where(t => this.ItemSelector(t)
                         .IndexOf(_criteria, StringComparison.OrdinalIgnoreCase) >= 0))
         .Skip(_skipCount)
         .ToList();

     public Task<IList<object>> GetSuggestionsAsync(
         string criteria,
         CancellationToken cancellationToken)
    {
        _criteria = criteria;
        return this.GetSuggestionsAsync(cancellationToken);
    }

    public Task<IList<object>> GetSuggestionsAsync(CancellationToken cancellationToken)
    {
        var newItems = this.NewItems;
        ...

        return Task.FromResult<IList<object>>(newItems.Take(batchSize).Where(t => ...
    }

Apparently the overloads of GetSuggestionAsync are very similar, so I took the freedom to let one call the other.

If you omit to set the ItemSelector a default value is used.

Usage:

var customSuggestionProvider = new CustomSuggestionProvider<MySuggestion>(...)
{
    ItemSelector = t => t.Name,
};

Or:

var customerSuggester = new CustomSuggestionProvider<Customer>(...)
{
    ItemSelector = customer => customer.City,
};

By the way, why do you ToList all your NewItems, if you only use the first batchSize ones? So skip the first 5 T objects, put the remaining 995 in a list and use only the first 10 of them (= indexes 5..14). Consider to remove the ToList part.

If you want, you can add ItemSelector to your constructor. I'm not sure if using constructors with a lot of properties will make your code more readable. For years there is a tendency to make constructors smaller and provide properties to set the values that you would otherwise have to set in a constructor. Provide obvious default values for these properties. You already do something like this with _criteria.

Upvotes: 1

Enigmativity
Enigmativity

Reputation: 117124

By introducing a generic type T you are saying that you will know what the type is when you call the method. So therefore, you also know what property you are trying to run the criteria against. It's easy to then just simply introduce a Func<T, string> getName delegate.

Now your first method looks like this:

public Task<IList<object>> GetSuggestionsAsync(string criteria, Func<T, string> getName, CancellationToken cancellationToken)
{
    _criteria = criteria;
    var newItems = _source.Where(x => getName(x).IndexOf(_criteria, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
    if (cancellationToken.IsCancellationRequested)
        return null;
    HasMoreSuggestions = newItems.Count > batchSize;
    _skipCount = batchSize;
    return Task.FromResult<IList<object>>(newItems.Take(batchSize).Cast<object>().ToList());
}

THere seems to be a lot of work here to produce batches of items that you aren't actually doing anything async with to get. You might as well use IEnumerable<IEnumerable<T>> instead of repeatedly calling Task<IList<object>>.

To do this, start with this extension method:

public static IEnumerable<IEnumerable<T>> Buffer<T>(this IEnumerable<T> source, int size)
{
    IEnumerable<T> GetBuffer(IEnumerator<T> enumerator)
    {
        var i = 0;
        do
        {
            yield return enumerator.Current;
        } while (++i < size && enumerator.MoveNext());
    }
    var e = source.GetEnumerator();
    while (e.MoveNext())
    {
        yield return GetBuffer(e);
    }
}

Then you can write this:

public IEnumerable<IEnumerable<T>> GetSuggestions(string criteria, Func<T, string> getName) =>
    _source
        .Where(x => getName(x).IndexOf(criteria, StringComparison.OrdinalIgnoreCase) >= 0)
        .Buffer(batchSize);

Much simpler and possibly even more performant.

Upvotes: 0

Related Questions