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 IEnumerable
s 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
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);
});
}