Skip to main content
Version: 4.2.5

Using Entity filters in Web APIs

The iCore Public API contains classes and methods that can be used to retrieve results from an Entity filter.

Adding a reference to an Entity filter

To use an Entity filter in a Web API, you must first add a reference to the filter, in the same way that you would add references to an assembly. For more information, see Adding a reference to an entity filter.

Filter API overview

When a filter has been referenced, you have access to a class generated from the Entity filter that represents its definition implementing IFilterQueryDefinition<TQuery> or IFilterQueryDefinition<TQuery,TParameters>, depending on if the filter contains parameters or not. This class can be used to retrieve results from the filter. The class is placed in the iCore.LocalSystem.EntityFilters namespace. The name of the class is a combination of the Entity filter name and its CLR name. For example, a Node filter with CLR name AllOrders generates the class Node\_AllOrders in the namespace iCore.LocalSystem.EntityFilters.

The query definition class contains nested classes representing the parameters of the filter (if any) as well as the rows returned from the filter. These classes are named Parameters and Row.

To create and execute a query, you call the CreateQuery method CreateQuery(FilterQueryOptions) or  CreateQuery(TParameters,FilterQueryOptions) which returns a disposable instance of an IFilterQuery<TEntity,TRow> instance which contains methods for executing and retrieving results from the filter.

Results can be retrieved in the following ways:

  • GetKeys which retrieves only the IEntityKeys for the entities of the filter.
  • GetRows which returns the results a list of rows containing the columns defined in the filter.
  • GetEntities which returns the actual entity instances for the entities represented by the rows of the filter result.

See Executing Filters for more information.

A query instance caches the results once retrieved, either in memory or on disk depending on the options specified to the query definition constructor. For example, multiple calls to GetRows() will always return the same rows if no additional calls to the database are made. See Caching for details.

The query definition also supports internal caching of query results, so that for example the same query results will be reused for multiple calls to CreateQuery for a specific time period. Internal caching prevents redundant calls to the database in cases when you do not necessarily need the latest results (for example when you have a high load of callers that can accept somewhat outdated data). See Caching for details.

The following example shows how you can build a simple controller containing an action that returns a list containing the names of all Node types, based on the Node type Entity filter "All".

[Route("api/nodeTypes")]
public class NodeTypeController : Controller
{
private readonly NodeType_All m_nodeTypeFilter;
public NodeTypeController(ISystemEx systemContext)
{
m_nodeTypeFilter = new NodeType_All(systemContext);
}
[HttpGet("all")]
[SwaggerResponse(typeof(string[]))]
public async Task<IActionResult> GetNodeTypes()
{
List<string> nodeTypeNames;
using (var query = m_nodeTypeFilter.CreateQuery())
{
nodeTypeNames = await query.GetRowsAsync()
.Select(row => row.Name) // Select just the Name property from each row
.ToListAsync(); // Asynchronously convert to a List<string>.
}
return Json(nodeTypeNames);
}
}

We recommend reusing the same instance of the query definition for all methods in the Web API, or at least all methods in the same Controller, that share the same caching policy (see Caching). The easiest way is to construct an instance of it in the constructor of the controller and simply keep it in a private field (as in the example above). You can also construct it in the Startup class and use dependency injection to inject it into your Controllers.

Caching

Every executed query caches its results when they are retrieved, at least for the lifetime of the query. This means that any two consecutive calls to for example GetRows() on the same query instance will return the same results, and a database roundtrip will not be made for subsequent calls. The default is to cache the results in memory for small result sets, and in a temporary file on disk for larger result sets, but this behavior can be modified and controlled in the FilterQueryCacheOptions specified to the filter query definition constructor. In addition, the filter query definition can be configured to cache query results between invocations of CreateQuery, so that they can for example be reused between multiple Web API calls.

Using the QueryCacheEnabled and QueryCacheAbsoluteExpiration options of FilterQueryCacheOptions lets you keep a query live for a specified time. Every time a query is executed with the same parameters (by any controller) during this time, the same results are returned without a roundtrip to the database. Once the time has expired, the next request for query results will cause a new roundtrip to the database and the cached results will be updated. This caching of data provides faster responses in highly concurrent Web API:s with many requests for the same data, where somewhat out-of-date data is acceptable.

note

All caching is per running instance of a Web API entity. Caches are not shared between Web APIs. For more information about the various options for controlling caching see FilterQueryCacheOptions.

note

Entities retrieved via GetEntities are not cached. See Retrieving entities.

Executing filters

Executing a filter and retrieving its results is done on the instance of IFilterQuery<TEntity, TRow> returned by calling CreateQuery on the filter query definition instance.

note

It is important to always dispose a returned query once you are done with it to free up resources. Make a habit of always placing queries in a using-block.

There is no need to manually buffer the enumeration results by calling ToList() or similar on the resulting IEnumerables returned by the query, since the query caches the results internally.

Retrieving keys

If you only need the entity keys representing the entities returned from a filter, use the GetKeys() method on the query, or one of the overloads of GetKeysAsync().

Example:

using (var query = m_nodeTypeFilter.CreateQuery())
{
foreach (IEntityKey key in query.GetKeys())
{
// ... do something with the key
}
}

Retrieving rows

If you want access to the information contained in the columns defined in the filter, use GetRows() or one of the GetRowsAsync() overloads. These methods return instances of the nested Row class which contains properties representing the columns of the Entity filter.

Example:

using (var query = m_nodeTypeFilter.CreateQuery())
{
foreach (NodeType_All.Row row in query.GetRows())
{
string name = row.Name;
DateTime modified = row.Modified;
IEntityKey key = row.Key;
// Do something with the row information....
}
}

Retrieving entities

If you want to retrieve the full entities of a query, you can use GetEntities() or GetEntitiesAsync() of the query. However, these methods do not cache the returned entities internally, instead a separate database call is made for each entity retrieved. For this reason, we do not recommend that you use these methods for large result sets.

note

For performance reasons, we recommend using GetKeys() or GetRows() instead of GetEntities when possible.

Example:

using (var query = m_nodeTypeFilter.CreateQuery())
{
foreach (INodeType row in query.GetEntities())
{
// Do something with the node Type entity...
}
}

Specifying parameters

If a filter has any parameters defined these must be specified when calling CreateQuery on the filter definition. The parameters are defined by a class named Parameters nested within the filter query definition class, and contains one property per parameter defined on the filter.

Example:

// The AllByLevel log filter in this example has a single parameter named LogLevel
var parameters = new LogEntry_AllByLevel.Parameters { LogLevel = iCore.Public.Types.LogLevel.Warning; };
using (var query = m_logFilter.CreateQuery(parameters))
{
// Use the query here.
}

Limiting the number of results (Record limit)

CreateQuery has an overload that accepts an instance of FilterQueryCacheOptions. The overload can be used to place a record limit on the number of records returned, which can be useful to avoid retrieving a large result set from the iCore system database when only a few rows are required.

Consider for example getting the latest 20 error logs, using a log filter named AllErrors:

var queryOptions = new FilterQueryOptions(top: 20);
using (var query = m_logFilter.CreateQuery(options))
{
// Use the query here.
}

Dependency injection

If you want to share the same instance of an Entity filter query definition, you can create it in your Startup class in the ConfigureServices method and add it to the service collection to be available for dependency injection in controllers. Caching Entity filter query definitions in this way is a good option unless there are other considerations that constrain the sharing of cached Entity filter query definitions between controller instances in the Web API.

Dependency injection requires an instance of the current system context (ISystemEx) instance, which can be injected into the constructor of your Startup class.

Example:

    public class Startup
{
public Startup(IHostingEnvironment env, ISystemEx system)
{
SystemContext = system;
/// ... Other initialization here
}
public void ConfigureServices(IServiceCollection services)
{
// ... other service configuration left out for brevity
FilterQueryCacheOptions cacheOptions = new FilterQueryCacheOptions()
{
QueryCacheEnabled = true,
QueryCacheAsboluteExpiration = TimeSpan.FromMinutes(1),
QueryCacheSlidingExpiration = null
};
services.AddSingleton<NodeType_All>(new NodeType_All(SystemContext, cacheOptions));
}
/// ... other methods left out for brevity.
}

In your controllers you can then directly inject the filter query instance (NodeType_All in this case) in the constructor.

[Route("api/nodeTypes")]
public class NodeTypeController : Controller
{
private readonly NodeType_All m_nodeTypeFilter;
public NodeTypeController(ISystemEx systemContext, NodeType_All nodeTypeFilter)
{
m_nodeTypeFilter = nodeTypeFilter;
// ... other initialization left out for brevity
}
// ...

Using IAsyncEnumerable sequences

Some overloads of the methods for retrieving result sets return an IAsyncEnumerable<T>, for example IFilterQuery.GetRowsAsync(). We recommend using these enumerables when you write async actions in a controller, to allow for more efficient processing while awaiting I/O operations such as database calls.

While it is not possible to use the familiar foreach statement with such sequences (in versions earlier than C# 8, which is not currently supported), there are several extension methods available in System.Linq.AsyncEnumerable in the System.Linq.Async assembly which are referenced by default when you add a reference to an entity filter.

Below are some examples to get you started on working with IAsyncEnumerable<T> sequences.

    using (var query = m_nodeTypeFilter.CreateQuery())
{
// Retrieve the rows from the query.
var rows = query.GetRowsAsync();
// Get all the names of the rows into a list.
var nodeTypeNames = await rows
.Select(row => row.Name) // Select just the Name property from each row
.ToListAsync(); // Asynchronously convert to a List<string>.
// Get the first row (or null if sequence is empty)
var firstRow = await rows.FirstOrDefaultAsync();
var rowCount = await rows.CountAsync();
// Enumerate all rows, and perform non-asynchronous actions with each row only:
await rows.ForEachAsync(row =>
{
// do something with each row
});
// Enumerate all rows, and perform asynchronous actions in the body:
await rows.ForEachAwaitAsync(async row =>
{
// Dummy async action.
await Task.Delay(1000);
});
}