5
\$\begingroup\$

I want to search some or all public properties of a class and for this requirement I have implemented a Property Searcher.

The Input for a Search is always a string. The Output contains the SearchString, the SearchContent in which the SearchString is contained and the searched Property.The Output is wrapped in a SearchResult object.


This is what i have implemented:

SearchResult.cs

public class SearchResult
{
    public PropertyInfo Property { get; private set; }

    public string SearchString { get; private set; }

    public string SearchContent { get; private set; }

    public ISearchable Item { get; private set; }

    public SearchResult(ISearchable item, 
                        PropertyInfo property, 
                        string searchString)
    {
        this.Property = property;
        this.SearchString = searchString;
        this.Item = item;
        this.SearchContent = property.GetValue(item).ToString();
    }
}

ISearchable.cs

public interface ISearchable
{
    IEnumerable<SearchResult> Search(string searchString);
}

Extension Methods for easier usage:

Extension.cs

public static class Extension
{
    public static SearchResult Search(this ISearchable source, string searchString, PropertyInfo info)
    {
        if (info.GetValue(source).ToString().Contains(searchString, StringComparison.OrdinalIgnoreCase))
        {
            return new SearchResult(source, info, searchString);
        }

        return null;
    }

    public static IEnumerable<SearchResult> Search(this IEnumerable<ISearchable> source, string searchString)
    {
        return source.SelectMany(searchable => searchable.Search(searchString)).Where(x => x != null);
    }

    public static bool Contains(this string source, string toCheck, StringComparison comp)
    {
        return source.IndexOf(toCheck, comp) >= 0;
    }
}

I also want to show you the example and test code for e.g. searching in the property of a Book with a Description (the code is simplified and more complex in the original project).

Book.cs

public class Book : ISearchable
{
    public DescriptionInfo DescriptionInfo { get; set; }
    public string Title { get; set; }

    public IEnumerable<SearchResult> Search(string searchString)
    {
        yield return this.Search(searchString, this.GetType().GetProperty(nameof(this.Title)));
        foreach (var searchResult in this.DescriptionInfo.Search(searchString))
        {
            yield return searchResult;
        }
    }
}

Description.cs

public class DescriptionInfo : ISearchable
{
    public string Description { get; set; }

    public string ShortDescription { get; private set; }

    public DescriptionInfo(string description)
    {
        if (!string.IsNullOrEmpty(description))
        {
            this.Description = this.ShortDescription = description;
        }
    }

    public IEnumerable<SearchResult> Search(string searchString)
    {
        yield return this.Search(searchString, this.GetType().GetProperty(nameof(this.Description)));
    }
}

Program.cs

internal class Program
{
    private static void Main(string[] args)
    {
        var one = new Book
        {
            Title = "This Test Title is Amazing",
            DescriptionInfo = new DescriptionInfo("This i a nice Test Book Description")
        };
        var two = new Book
        {
            Title = "This second Test Title is Amazing",
            DescriptionInfo = new DescriptionInfo("This i a nice Book Description")
        };
        var three = new Book {Title = "Boring Title", DescriptionInfo = new DescriptionInfo("No Description")};

        var list = new List<Book>(new[] {one, two, three});
        var results = list.Search("Test").ToList();
    }
}

In results are three SearchResult objects. The string "Test" was found two times in a Title Property and one time in the Description Property of the Description class.


I do not know if this is the best solution to solve the problem. If you have a better one in mind please let me know.

\$\endgroup\$
2
\$\begingroup\$

I don't have a C# compiler at hand and cannot test the following.

It looks like the Search() always returns the sum of results of these 3 things:

  1. delegate the call to any property that's ISearchable.
  2. search the value of some properties
  3. do not search the value of some other properties

The method will likely always consist of a list of these types of code blocks:

  1. the delegate type

    foreach (var searchResult in this.PROPERTY_THAT_IS_ISEARCHABLE.Search(searchString))
    {
        yield return searchResult;
    }
    
  2. for the direct search

    yield return this.Search(searchString, this.GetType().GetProperty(nameof(this.PROPERTY_THATS_NOT_ISEARCHABLE)));
    
  3. no code for excluded properties

This smells like copy&paste. But you are already doing reflection, so why not use it to your advantage?

I think you can create a generic Search() method, then slap that onto everything that implements ISearchable via extension, getting you rid of explicitely implementing the method in those classes. Here's how:

  1. You should be able to filter out all properties that implement ISearchable and delegate the method call to them.
  2. For non-ISearchable properties that should be searched, you should decide if they are the minority1. If so, define an attribute to opt-in a property. You'd then ignore all non-ISearchable by default and only search the values of those that are explicitly opted-in with the attribute.
  3. For non-ISearchable properties that should not be searched, do pretty much the opposite of (2.). If they are the minority, create and add to them an attribute explicitly to opt-out of the default which would be to search all non-ISearchable properties.

1 The minority thing is only to reduce future work, minimizing the amount of attributes that you have to add. Attributes are optional, so you have to define some default behaviour anyway.

Let's say the default behaviour is to search all properties and opt-out via attribute. Here's how your classes could look like:

public class Book : ISearchable
{
    public DescriptionInfo DescriptionInfo { get; set; }
    public string Title { get; set; }
}

As you can see in the Book class: ISearchable is more or less a marker interface.

public class DescriptionInfo : ISearchable
{
    public string Description { get; set; }

    [IgnoreThisDuringSearch]
    public string ShortDescription { get; private set; }

    public DescriptionInfo(string description)
    {
        if (!string.IsNullOrEmpty(description))
        {
            this.Description = this.ShortDescription = description;
        }
    }
}

The generic extension method would perform the following two steps then:

  1. delegate to every property that it can be delegated to
  2. search every property that got opt-in via attribute or search every property except those opt-out via attribute

Some additional thoughts:

The attribute makes the search independent from the actual names of the properties. If you change Title to Totle in the Book class, you current code breaks. The suggested solution is solely based on reflection and existance (lack of) an attribute, which should always work, given a sensible default behaviour.

If you wish to perform the searching/delegating in a special order, you could do that, too. For example via a property of another attribute. See [DataMember(Order = n)] for example.

\$\endgroup\$
  • \$\begingroup\$ You mentioned a few very good points and i have also thought about implementing an attribute to identify the searchable properties. i will try this implementation. \$\endgroup\$ – Jens Feb 15 '16 at 22:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.