The legacy AFEventFrame.FindEventFrames methods were introduced beginning with AF SDK 2.4.  An enhanced search feature was introduced in AF SDK 2.8 with the AFEventFrameSearch class.  This blog will show examples using both the legacy syntax and the new class.  As the examples strive to be near equivalents between the legacy versus new search class, these examples may serve as a Rosetta Stone for developers who are familiar with one method and want to learn about the other.


This blog is part of the series Working with PI AF SDK 2016 - Introduction to the blog post series .


Legacy Methods

There are many different legacy AFEventFrame methods for finding event frames.  Briefly:

  • FindEventFrames
  • FindEventFramesByAnalysis
  • FindEventFramesByAttribute
  • FindEventFramesByExtendedProperty
  • FindEventFramesByPath
  • FindEventFramesByReferenceType


There is also one more set of legacy search methods that's not in the AFEventFrame class.  Rather this method is found in the AFElement class.

  • GetEventFrames


Existing code will still work with the legacy methods, so there is no need to change it for change's sake.  Going forward you are strongly encouraged to use the new

AFEventFrameSearch methods since the help for some of the legacy methods will display this advice:


Important note Important
Consider using the new AFEventFrameSearch class for finding event frames instead of using this method.


This blog will show examples for the GetEventFrames, FindEventFrames, and FindEventFramesByAttribute legacy methods, and the corresponding AFEventFrameSearch methods.


Basic Event Frame Searching

The first section of examples will perform basic searching based on a time range, referenced element, and event frame template.  Much further below, we will later expand to more advanced searching by modifying our search to filter on an attribute's value.


The following properties will be used in my example code.  Note that the properties are indeed correctly set when I execute the code.  Trust me.


public PISystem AssetServer { get; set; }
public AFDatabase Database { get; set; }
public AFElement Element { get; set; }
public AFElementTemplate Template { get; set; }
public AFTime StartTime { get; set; }
public AFTime EndTime { get; set; }


The above is included to help the reader follow along in code samples.  On my actual C# 6.0 test system, my code looks more like:


public PISystem AssetServer => Element?.PISystem;
public AFDatabase Database => Element?.Database;
public AFElement Element { get; }


But users that don't have C# 6.0 ask too many questions like "What does '=> Element?.PISystem' mean?"  Hence I've given simplified declarations so as not to get bogged down on side topics such as what is an expression-bodied auto-property or what is a null-conditional operator.


Legacy GetEventFrames Example

This example would find event frames that reference the specified Element based on the specified event frame Template.  Note that Element does not have to be the primary referenced element.  It only has to be in the event frame’s ReferencedElements collection.


AFNamedCollectionList<AFEventFrame> frames;
frames = Element.GetEventFrames(
            searchMode: AFSearchMode.Overlapped,
            startTime: StartTime,
            endTime: EndTime,
            nameFilter: null,
            elemCategory: null,
            elemTemplate: Template,
            sortField: AFSortField.StartTime,
            sortOrder: AFSortOrder.Ascending,
            startIndex: 0,
            maxCount: 1000);


Note if you desired paging, you would need to incorporate it yourself in a loop where you would adjust startIndex each iteration.  The newer methods have paging baked in.


Legacy FindEventFrames Example

The equivalent code using the legacy AFElement.FindEventFrames method would be:


AFNamedCollectionList<AFEventFrame> frames;
frames = AFEventFrame.FindEventFrames(
            database: Database,
            searchRoot: null,
            searchMode: AFSearchMode.Overlapped,
            startTime: StartTime,
            endTime: EndTime,
            nameFilter: null,
            referencedElementNameFilter: Element.Name,
            eventFrameCategory: null,
            eventFrameTemplate: Template,
            referencedElementTemplate: Element.Template,
            durationQuery: null,
            searchFullHierarchy: true,
            sortField: AFSortField.StartTime,
            sortOrder: AFSortOrder.Ascending,
            startIndex: 0,
            maxCount: 100000);




AFEventFrameSearch Without Server-side caching

The new search class will show an equivalent search, but make note that the returned type will be an IEnumerable<AFEventFrame> rather than an AFNamedCollectionList<AFEventFrame>.  Note if you must work with an entire list after making this call, you always have the ability to issue a ToList() on the returned frames.  Whether you have the RAM is another question.  If you were expecting a small enough collection, ToList is fine.  If you were expecting a large collection, you would want to enable server-side caching as well has specify paging.


string query = string.Format("Element:{0} Template:{1}", Element.GetPath(Database), Template.GetPath(Database));

var search = new AFEventFrameSearch(Database, "optional name", AFSearchMode.Overlapped, StartTime, EndTime, query);

IEnumerable<AFEventFrame> frames = search.FindEventFrames(startIndex: 0, fullLoad: false, pageST


See how easy it is to implement paging with the new methods!  Note there is always paging enabled for this method.  Specifying a pageSize of 0 does not turn off paging.  Rather it will default to the page size set to the current value of CollectionPageSize.


When fullLoad is false, you're only getting back basic header info of event frames.  This would be the inherent properties such as Name, Description, Template, etc, but not any extended properties nor any attribute info.  You could consider using the AFEventFrame.LoadEventFrames method but not only does this require a 2nd trip to server, it only accepts an IList<AFEventFrame> when what you have is IEnumerable<AFEventFrame>.  If you were planning on just displaying header properties for the event frames, fullLoad: false is acceptable.


But if you want to display any attribute info, you would want to use fullLoad: true.  The need is not limited to just referencing an attribute directly, as item["Tank Level"].  You want fullLoad: true if you used anything like item.Attributes.Count or item.DefaultAttribute.



AFEventFrameSearch With Server-side caching

A natural question would be "How do you know that the previous example does not have server-side caching?"  Because we didn't specify it, that's why!  To do that, all we do is add one line of code to the previous example:


string query = string.Format("Element:{0} Template:{1}", Element.GetPath(Database), Template.GetPath(Database));

var search = new AFEventFrameSearch(Database, "optional name", AFSearchMode.Overlapped, StartTime, EndTime, query);
search.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

IEnumerable<AFEventFrame> frames = search.FindEventFrames(startIndex: 0, fullLoad: false, pageSize: 1000);


See line 4.  Also see Considerations section towards the bottom for a discussion about server-side caching.



Advanced Searching on an Attribute Value

The previous examples demonstrated a basic search of event frames based on time range, event frame template, and referenced element.  With the basics down, we can turn our attention to more advanced searching based on an attribute's value.


Legacy FindEventFramesByAttributes Example

In the code below, I want to search the same set of event frames that I found earlier in the FindEventFrames example, only now I want to get a filtered subset of those event frames when the attribute "Tank Level" has a value greater than or equal to 75.  The trick here is to define an AFAttributeValueQuery to perform a requested comparison on a requested attribute template.


AFAttributeValueQuery[] valueQuery = new AFAttributeValueQuery[] 
                                        { new AFAttributeValueQuery(Template.AttributeTemplates["Tank Level"]
                                          , AFSearchOperator.GreaterThanOrEqual
                                          , 75.0)

AFNamedCollectionList<AFEventFrame> frames;
frames = AFEventFrame.FindEventFramesByAttribute(
            searchRoot: null,
            searchMode: AFSearchMode.Overlapped,
            startTime: StartTime,
            endTime: EndTime,
            nameFilter: null,
            referencedElementNameFilter: Element.Name, 
            durationQuery: null,
            valueQuery: valueQuery,
            searchFullHierarchy: true,
            sortField: AFSortField.StartTime,
            sortOrder: AFSortOrder.Ascending,
            startIndex: 0,
            maxCount: 1000);


Tread carefully here.  It is impossible to perform paging correctly with this method if the event frames have attribute values from a data reference that are not captured.



AFEventFrameSearch By Attribute Example

I could show 2 very similar examples using AFEventFrameSearch: one with caching and one without.  But since only 1 simple line of code differs, I am going to only show the example with caching.  You will note that it looks very similar to our previous AFEventFrameSearch example with the notable exception of one new line of code:


string query = string.Format("Element:{0} Template:{1}", Element.GetPath(Database), Template.GetPath(Database));
query += " '|Tank Level':>=75";  //be sure to include leading blank

var search = new AFEventFrameSearch(Database, "optional name", AFSearchMode.Overlapped, StartTime, EndTime, query);
search.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

IEnumerable<AFEventFrame> frames = search.FindEventFrames(startIndex: 0, fullLoad: false, pageSize: 1000);


Line 2 has a literal string specifying the attribute name and value to check.  You may, of course, be working with variables to make the code more flexible.  In that case, you would probably have something like:


string attrName = "|Tank Level";
double threshold = 75.0;
query += string.Format(" '{0}':>={1}", attrName, threshold);  //be sure to include leading blank


The astute reader will note that a StringBuilder could also be used, if one desired to break the query onto different lines.  In such cases, an AppendFormat would be preferred over AppendLine.



Other Considerations


Server-side Caching

Barry Shang explains caching in his blog Why you should use the new AF SDK Search.  I am echoing his original remarks but noted for AFEventFrameSearch instead.

  • The new search allows you to opt in to caching object identifiers of found event frames on the server. This is done via setting the AFEventFrameSearch.CacheTimeout property.  This effectively takes a "snapshot" of the found collection, caches it, and provides a server-side enumerable collection that the AF client can consume. Caching of identifiers allows SQL Server to retrieve subsequent pages faster by avoiding repetitive queries. The traditional search which does not implement caching will incur more overhead on the SQL Server side when  getting multiple pages of items.
  • Should you opt in to caching when using the new search? Let's see what the docs for CacheTimeout say: If you will only be getting items from the first page, then it is best to leave the cache disabled. If you will be paging through several pages of items, then it is best to enable the cache by setting this property to a value larger than you expect to be using the search.


What is being returned by the AFSearch is an Enumerable collection, and not just a List<AFEventFrame> which happens to implement the IEnumerable<AFEventFrame> interface.  Instead the Enumerable collection is being re-fetched each time you iterate over it, either explicitly with a foreach or implicitly with LINQ calls such as Select or Where.  It depends upon the collection size being returned, if you enabled caching, and what you intend to do with the returned items each time. If a very large number will be returned, you probably won’t have enough RAM to keep them all loaded in memory. In this case you probably want to enable caching, not keep a strong reference to items already processed, and let the garbage collector free memory when needed. If the number is smaller, then you will have better performance if you cache the returned list if you are going to make repeated iterations over the list.


Lastly, if you do find yourself iterating over the Enumerable collection more than once, be sure to issue an AFServer.Refresh before each subsequent pass over if you want to see newly added items in the returned collection. This would refresh the search that is cached on the server so that it would pick up any added objects.



When Element Name is not unique

When we are searching on element name with AFEventFrameSearch, you will note my code does not use the AFElement.Name property but instead gets the path relative to the current database.  This would be Element.GetPath(Database) which for my example is equivalent to Element.GetPath(Element.Database).


Whereas for the legacy FindEventFrames, I use the element's Name with the named argument referencedElementNameFilter: Element.Name.  That works for my simplified examples where my test database only has 5 elements, all on the root of the database, and all of them are uniquely named.  Since those names are unique, I am guaranteed that the returned event frames only belong to the element I care about.  A real world database would not be so lucky.  If the element names were not unique, what can you do ensure uniqueness?


In the help for FindEventFrames, there is this interesting part regarding the name:


referencedElementNameFilterType: System.String

The name filter string of an AFElement in the event frame's ReferencedElements collection. Uses same filtering rules as specified for the nameFilter parameter. To obtain an exact match to a specific element, specify the element's ID using a format that includes the opening and closing braces, such as ID.ToString("B").


So why didn't I used Element.ID.ToString("B") instead of Element.Name?  Because I isolated all of my legacy tests against an AF 2.6 server, and all the newer AFEventFrameSearch tests were done against an AF 2.8 server.  What I discovered later is that Element.ID.ToString("B") was added in AF 2.7.5 server.  For the newer AFEventFrameSearch tests running on my 2.8 server, I could have interchanged Element.ID.ToString("B") with Element.GetPath(Database) since both return a unique element.



Using AFSearchTokens

The examples for AFEventFrameSearch used this constructor mainly because it allowed the AFSearchMode.Overlapped.  You can use the constructor that relies upon search tokens but now you must also specify the dates as search tokens.  This code here is longer and looks messier.  But it is an option to you.


IEnumerable<AFEventFrame> frames;

List<AFSearchToken> tokenList = new List<AFSearchToken>();

tokenList.Add(new AFSearchToken(AFSearchFilter.Element, AFSearchOperator.Equal, Element.GetPath(Database)));
tokenList.Add(new AFSearchToken(AFSearchFilter.Template, AFSearchOperator.Equal, Template.GetPath(Database)));
// Given nature of AND-ing tokens, this is how Overlapped would work:
tokenList.Add(new AFSearchToken(AFSearchFilter.Start, AFSearchOperator.LessThanOrEqual, EndTime.UtcTime.ToString("O")));
tokenList.Add(new AFSearchToken(AFSearchFilter.End, AFSearchOperator.GreaterThanOrEqual, StartTime.UtcTime.ToString("O")));
// Search by value:
tokenList.Add(new AFSearchToken(AFSearchFilter.Value, AFSearchOperator.GreaterThanOrEqual, "75", "|Tank Level"));

AFEventFrameSearch search = new AFEventFrameSearch(Database, "optional name", tokenList);
search.CacheTimeout = TimeSpan.FromMinutes(10); // Opt in to server-side caching

frames = search.FindEventFrames(startIndex: 0, fullLoad: false, pageSize: 1000);


The earlier examples passed a string rather than a token list.  That  query string will be parsed into a token list using the AFEventFrameSearch.ParseQuery method.

If you want to see which search tokens were specified or generated from a query string, use the AFSearch.Tokens property.



Related Links


Search Query Syntax Overview

AFSearchToken Structure

AFSearchOperator Enumeration

AFSearchFilter Enumeration