Write a custom .NET attribute to mark searchable columns of a HTML table and match them with their equivalent DB columns

Consider you have to implement the following requirement:

  • We have a MVC .NET web application and a view with a HTML table and multiple columns. We want to mark some of these columns as searchable, so that we can search their values when we use the search field.

  • We host our data in a relational database, we get the structure of the DB table as it is. We use Entity Framework and we need to “map” the DB columns to their equivalent UI columns, so that the search works correctly.

Here a simple mockup of our UI:

Balsamiq mockup for article about .NET attribute and CallerMemberName

We are going to implement a .NET attribute to give the properties of our UI model class some metadata.

1
2
3
4
5
6
7
8
9
10
[AttributeUsage(AttributeTargets.Property)]
public class SearchableAttribute : Attribute
{
    public string DBColumnName { get; }

    public SearchableAttribute(string dbColumnName = null, [CallerMemberName] string uiColumnName = null)
    {
        DBColumnName = dbColumnName ?? uiColumnName;
    }
}

We restricted the use of the attribute to properties only. The Searchable attribute can get up to two arguments.

  • The first parameter is the name of the column of the database. This is the mapping between the UI and the DB and this is an optional argument.

  • The second argument uses a very cool feature which is the CallerMemberName (Here you can find more information about it) attribute. This gives us the name of the attribute’s caller. In our case this is the name of each property on which we apply the Searchable attribute. This name is going to be the fallback name used for the name of the DB column.

Now lets apply the attribute to our simple model class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
    [Searchable]
    public string Firstname { get; set; }

    [Searchable("Name")]
    public string Lastname { get; set; }

    public string Sex { get; set; }

    [Searchable]
    public int Age { get; set; }

    public string Country { get; set; }    
}

We made three out of five properties of our model class searchable and as you can see, the Lastname property has another name in the database. We define this name as the first argument of the attribute.

We are ready to write a helper function which gets the name of the UI column and based on reflection it finds the mapping to the equivalent DB column by using the Searchable attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static string MapUIColumnNameToDBColumnName<T>(string uiColumnName)
{
    var modelProperties = typeof(T).GetProperties();

    try
    {
        var dbColumnName = modelProperties.Single(m => m.Name == uiColumnName)
            .GetCustomAttributes(typeof(SearchableAttribute), false).Cast<SearchableAttribute>()
            .SingleOrDefault()?.DBColumnName;

        return dbColumnName;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

The MapUIColumnNameToDBColumnName works as follows:

  • We first get the properties of the model class. We use Generics so that we can apply this helper function to every model class of our application.

  • We then find the property by name, using the uiColumnName.

  • We call the GetCustomAttributes function and we focus only on the attributes of type SearchableAttribute.

  • Since the GetCustomAttributes function returns a list of attributes of type object, we cast this list to the needed type, so that we can parse its properties.

  • We then search for one occurrence only and we read the DBColumnName of the attribute, if one was found.

  • We return the name to the caller.

To recap, we just saw two very helpful features we can use while working with C# and the .NET framework. The first was the CallerMemberName attribute and the second was the accessing of the custom attributes of the properties of a class by using reflection.

I hope you enjoyed this article. Write a comment if you have any thoughts.

comments powered by Disqus