Enkel och skalbar sökfunktion i .NET Core

Obs! Det här inlägget förutsätter att du har lite förkunskaper kring .NET Core, Linq och databaser för att kunna gå vidare. Har du koll på detta eller bara vill ha lite kul läsning så läs bara vidare.

Har du kommit på dig själv med att bara länka flera .Where(i => i.Text.Contains(search)) i dina Linq-queries för att få din sökning att fungera korrekt? Vi brukade göra det förut och det blev fort väldigt bökigt och fult. Självklart kan man använda sig av avancerade paket och distribuerade sökfunktioner för att få till en bra sökning, men ibland vill du bara ha något väldigt enkelt. Där har vi en lösning.

Nu till koden

Säg att du har databas med klasser som ser ut såhär:

public class Person 
{
    public string Name { get; set; }
    public string Email { get; set; }

    [MakeSearchable]
    public Facility Facility { get; set; }
}

public class Facility
{
    public string Name { get; set; }
}

Då kanske du vill kunna söka på Name, Email och Facility.Name. För att lösa det här på ett generaliserat sätt så kommer vi använda ett attribut som kommer heta MakeSearchable och sedan för att kunna hitta alla properties med det här attributet så gör vi två extensions. En för att hitta properties av en viss type och en generell. Koden bör då se ut så här:

[AttributeUsage(AttributeTargets.Property)]
public class MakeSearchable : Attribute
{
}

public static IEnumerable<PropertyInfo> SearchableProperties(this IReflect obj)
{
    return obj.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => Attribute.IsDefined(p, typeof(MakeSearchable)));
}

public static IEnumerable<PropertyInfo> SearchablePropertiesOfType<T>(this IReflect obj)
{
    return obj.SearchableProperties().Where(p => p.PropertyType == typeof(T));
}

Med attributet så kan vi nu dekorera våra klasser så att vi senare kan hitta dem med vår generaliserade sökfunktion så här:

public class Person 
{
    [MakeSearchable]
    public string Name { get; set; }

    [MakeSearchable]
    public string Email { get; set; }

    [MakeSearchable]
    public Facility Facility { get; set; }
}

public class Facility
{
    [MakeSearchable]
    public string Name { get; set; }
}

Nu har vi allt vi behöver för att påbörja vår sökfunktion. Vi kommer då göra en extension på en IQueryable<T> för att på så sätt få tillgång till den här funktionen globalt med import av rätt namespace. Vår statiska klass och metod för att söka i fält kommer att se ut så här:

public static class QueryableExtensions
{
    public static IQueryable<T> GetResultsFor<T>(this IQueryable<T> source,
                                                 string search)
    {
        // Hämta alla string-properties som är markerade med vårt attribut
        var childProperties = typeof(T).SearchablePropertiesOfType<string>()
                                        .Select(prop => prop.Name);

        // Hämta alla string-properties som är markerade med vårt attribut på
        // de relaterade objekt som är markerade med vårt attribut
        var childproperties = typeof(T).SearchableProperties()
                                        .SelectMany(prop => 
                                            prop.PropertyType.SearchablePropertiesOfType<string>()
                                                .Select(child => $"{prop.Name}.{child.Name}"));

        // Skapa en lista med alla properties samlade och
        // sen en sträng som gör .Contains på alla
        var properties = childProperties.Union(childproperties).Select(prop => $"{prop}.Contains(\"{search}\")").ToList();
        
        // Om det inte finns några properties att söka på
        // returnera då bara den ursprungliga listan
        if (!properties.Any()) return source;

        // Samla ihop alla söktermer med || för att då söka i alla fält            
        var query = string.Join("||", properties);
        return source.Where(query);
    }
}

För att förklara vad som händer så är det som följer: Vi hämtar alla fält och relaterade objekts fält som är markerade med MakeSearchable och samlar ihop dem. Vi itererar sen över den här listan och konkatenerar ihop fältet med prop.Contains(search) för att på så sätt söka i alla fält om de innehåller vår söksträng search.

Jag skapar en mock-databas med personer och sen kan jag enkelt i den genom att skriva:

public static class TestData
{
    public static List<Person> Persons()
    {
        var namn = new Person {Name = "Namn", Email = "namn@mail.com", Facility = new Facility {Name = "Anläggning1"}};
        var name = new Person {Name = "name", Email = "name@mail.com", Facility = new Facility {Name = "Anläggning2"}};
        return new List<Person> {name, namn};
    }
}

...

DataBase.Persons().Search("namn@mail.com");

Svårare än så är det inte att skriva en väldigt simpel och skalbar sökfunktion i .NET Core. Om du vill se mer eller experimentera själv så kolla gärna in vårt Github repo för projektet. Där har vi satt upp unit-tester för att se att allt fungerar också. Ha det gott tills nästa gång!