Strongly Typed Searching in ADAM, part 2

Remember part 1 of Søren Trudsø Mahon's article of DXP? Who's up for part two? Here goes:

In part 1 we saw how to use the API, in this post we’ll have a look at the implementation behind this.

The interface of the API

To do this, we've created a fluent API for searching ADAM objects: inspired by this series of blog posts:
http://www.lostechies.com/blogs/gabrielschenker/archive/2010/01/03/fluent-silverlight-fluent-api-and-inheritance.aspx
http://www.lostechies.com/blogs/gabrielschenker/archive/2010/01/03/fluent-silverlight-fluent-api-and-inheritance.aspx

To start off with we created an extension method (new feature in c# / .net 3.5) on the Application object like this:

C#
1
2
3
4
5
6
7
8
9
10
namespace Adam.Core 
{
    public static class ApplicationSearchExtensions 
    {
        public static AdamSearch Search(this Application @this) 
        {
            return new AdamSearch(@this);
        }
    }        
}

We have put this inside of the Adam.Core namespace, this way we don’t have to remember to include our namespace wherever we want to call Search on the Adam.Core.Application object. (We’ll probably end up putting the AdamSearch behind an interface, and having a factory for creating the AdamSearch, enabling us to mock our search). The AdamSearch class is our entry into searching, in here we define the root types you can search on:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AdamSearch 
{
    private readonly Application application;

    public AdamSearch(Application application) 
    {
       this.application = application;
    }
        
    public IUserSearchRoot User 
    {
       get { return new UserSearch(this.application); }
    }

    …
}

The class xxxSearch represents the adam object we are searching for, this is hidden behind an interface IxxxSearchRoot. The keywords are defined in interface, IUserSearchKeywords and ISearchKeywords:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface IUserSearchKeywords<TSearchParent, TItem, TCollection>
    : ISearchKeywords<TSearchParent, TItem, TCollection> 
{
    TSearchParent Name(string name);
}

public interface ISearchKeywords<TSearchParent, TItem, TCollection> 
{
    IUserSearchKeywords<TSearchParent, TItem, TCollection> CreatedBy 
    { 
      get; 
    }

    TSearchParent Id(Guid id);

    TSearchParent Id(Operator @operator, Guid guid);

    TSearchParent Id(IEnumerable<Guid> ids);

    TSearchParent CreatedOn(Operator @operator, DateTime time);
}

We have methods and properties which correspond to the keywords in the Adam Help. Properties are links to other objects. For example:
CreatedBy.Name("administrator") corresponds to createdby.name = administrator

Methods are actual criteria. For example:
Id(user.Id) correspondents to id = 'C899EFCB-440E-487F-A007-09A9043E678F'

Pretty simple, but showing that our API correspondents to the Adam search.

The TSearchParent generic parameter is used as a placeholder for the kind of search we are performing. That is, if we are performing a RecordSearch this will refer back to an IRecordSearch where the keywords for searching for records are defined. This enabled me to chain the keywords like this:

C#
1
2
3
Application.Search()
  .User.CreatedBy.Name("dxp-stm")
  .CreatedOn(Operator.LessThan, DateTime.Now);

Implementing the API:

We've created a “big” base class where all of the main loading functions and common keywords are implemented:

This class has the responsibility of:

  • All the search operators that are common to all Adam objects (Id, CreatedBy, CreatedOn...)
  • Executing queries (delegated to the SearchExecutor).
  • Collection all search expressions (delegated to SearchExpressionProvider)
  • Executing queries when we have no search expression yet (Load(Guid Id) and Load(IEnumerable<Guid> ids).

Interesting bits are the implementation of keywords which lead to adding a search criteria to our search expression. But also how we refer to other ADAM objects.

Implementation of keywords in SearchBase.cs:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public TSearchParent Id(Guid id) 
{
    return AddEq("Id", id);
}

protected TSearchParent AddEq(string path, object value) 
{
            return Add(path, Operator.Equal, value);
}

protected TSearchParent Add(string path, Operator @operator, object value) 
{
    ExpressionProvider.Add(ParentPath + path, @operator, value);
    return this.SearchParent;
}

Implementation of keywords in SearchBase.cs -> SearchExpressionProvider:

C#
1
2
3
4
5
6
7
8
9
10
11
public void Add(string expression, Operator @operator, object value) 
{
    expression = string.Format("{0} {1} ?", expression, 
        GetOperator(@operator));
    Add(expression, value);
}

public void Add(string expression, params object[] parameters) 
{
    this.expressions.Add(new SearchExpression(expression, parameters));
}

Implementation of “references” to other objects go like this:

C#
1
2
3
4
5
6
7
8
9
public IUserSearchKeywords<TSearchParent, TItem, TCollection> CreatedBy 
{
    get 
    {
        return new UserSearch<TSearchParent, TItem, TCollection>(
            Application, ExpressionProvider, SearchParent, 
            ParentPath, "CreatedBy");
    }
}

And specifically for User we have two classes called UserSearch and UserSearch<TSearchParent,TItem, TCollection>:

The source for these are trivial, and the keywords are like in SearchBase, so it’s mostly constructors, but a couple of things to note about UserSearch is that this closes our chain of parents and inside the constructor it “closes” our chain of parents by setting this to parent. (And this is maybe what I hate most about our current implementation, I would like to put this responsibiliy within my base class, but I just can get my head around that).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class UserSearch
    : UserSearch<IUserSearch, User, UserCollection>, 
      IUserSearchRoot, IUserSearch 
{
    public UserSearch(Application app)
        : base(app) 
    {
        this.SearchParent = this;
    }
}

public class UserSearch<TSearchParent, TItem, TCollection>
    : SearchBase<TSearchParent, TItem, TCollection>,
      IUserSearchKeywords<TSearchParent, TItem, TCollection>,
      IUserItemLoader<TItem>,
      IUserSearchLoader<TItem, TCollection>
    where TItem : ExtendedItemBase
    where TCollection : ExtendedItemBaseCollection 
{
    public UserSearch(Application app)
        : base(app) 
    {
    }

    public UserSearch(Application app, SearchExpressionProvider expressionProvider, TSearchParent searchParent, string parentPath, string path)
        : base(app, expressionProvider, searchParent, parentPath, path) 
    {
    }

    #region Implementation of IUserSearchKeywords<TSearchParent,TItem,TCollection>

    public TSearchParent Name(string name) 
    {
        return AddEq("Name", name);
    }

    #endregion
}

And then we've got a lot of interfaces:

Basically we have interfaces for doing

  • Load(Guid id) and Load(IList<Guid> ids):
    IUserItemLoader<TItem>: IExtendedItemLoader<TItem>
  • A Load() using searchexpressions:
    IUserItemLoader<TItem>: IExtendedItemLoader<TItem>
  • Keywords:
    IUserSearchKeywords<TSearchParent, TItem, TCollection>: ISearchKeywords<TSearchParent, TItem, TCollection>

These interfaces are all implemented by UserSearch<TSearchParent, TItem, TCollection>, but we never expose that class in our API. That way we can control which operations are available to the consumer at anytime.

Conclusion

That’s where we are now, we got the foundation, but we still need to add keywords, but adding keywords is pretty easy, add it to the IXxxKeywords interface and then implement it in the class that “complaints”, so it’s something we will add along the way and whenever Adam adds new keywords.
And also we’ve created a Resharper template for creating a new Adam object for searching because the object structure and the interfaces needed are very similar, they only differ on the adam object name.

Sample Code

The article contains sample code project(s).
You must be logged in to view or download sample code.
Sign in now

Comments

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed