sources/Google.Solutions.IapDesktop.Core/EntityModel/Query/EntityQuery.cs (129 lines of code) (raw):
//
// Copyright 2024 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Apis.Locator;
using Google.Solutions.Common.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Solutions.IapDesktop.Core.EntityModel.Query
{
/// <summary>
/// Delegate for deriving an aspect from one or more input aspects.
/// </summary>
internal delegate object? DeriveAspectDelegate(IDictionary<Type, object?> aspectValues);
/// <summary>
/// Delegate for looking up entities.
/// </summary>
internal delegate Task<ICollection<TEntity>> QueryDelegate<TEntity>(CancellationToken cancellationToken)
where TEntity : IEntity<ILocator>;
/// <summary>
/// Queries an entity context using idiomatic syntax.
/// </summary>
public class EntityQuery<TEntity>
where TEntity : class, IEntity<ILocator>
{
private readonly EntityContext context;
private readonly QueryDelegate<TEntity> query;
private readonly Dictionary<Type, Func<ILocator, CancellationToken, Task<object?>>> aspects
= new Dictionary<Type, Func<ILocator, CancellationToken, Task<object?>>>();
private readonly Dictionary<Type, DeriveAspectDelegate> derivedAspects
= new Dictionary<Type, DeriveAspectDelegate>();
private EntityQuery(
EntityContext context,
QueryDelegate<TEntity> query)
{
this.context = context;
this.query = query;
}
private async Task<object?> QueryAspectAsync<TAspect>(
ILocator locator,
CancellationToken cancellationToken)
where TAspect : class
{
return await this.context
.QueryAspectAsync<TAspect>(locator, cancellationToken)
.ConfigureAwait(false);
}
private async Task<IList<EntityQueryResultItem<TEntity>>> ExecuteCoreAsync(
CancellationToken cancellationToken)
{
//
// Query entities.
//
var entities = await this
.query(cancellationToken)
.ConfigureAwait(false);
//
// Kick of asynchronous queries for each aspects and entity.
//
return await Task.WhenAll(entities
.EnsureNotNull()
.Select(e => EntityQueryResultItem<TEntity>.CreateAsync(
e,
this.aspects.ToDictionary(
item => item.Key,
item => item.Value(e.Locator, cancellationToken)),
this.derivedAspects))
.ToList());
}
/// <summary>
/// Include an aspect in the query so that its value
/// can later be accessed in EntityWithAspects.
/// </summary>
public EntityQuery<TEntity> IncludeAspect<TAspect>()
where TAspect : class
{
//
// Record that we need to query this aspect. We store a
// delegate (as opposed to just the aspect type) because
// we won't have access to type information later.
//
if (!this.aspects.ContainsKey(typeof(TAspect)))
{
this.aspects.Add(typeof(TAspect), QueryAspectAsync<TAspect>);
}
return this;
}
/// <summary>
/// Derive an aspect from another aspect.
/// </summary>
public EntityQuery<TEntity> IncludeAspect<TInputAspect, TAspect>(
Func<TInputAspect?, TAspect?> func)
where TInputAspect : class
where TAspect : class
{
//
// Make sure we query the input aspect.
//
IncludeAspect<TInputAspect>();
this.derivedAspects[typeof(TAspect)] = values =>
{
TInputAspect? input = null;
if (values.TryGetValue(typeof(TInputAspect), out var value))
{
input = value as TInputAspect;
}
return func(input);
};
return this;
}
/// <summary>
/// Execute query.
/// </summary>
/// <returns>
/// Snapshot of entities and their aspects.
/// </returns>
public async Task<EntityQueryResult<TEntity>> ExecuteAsync(
CancellationToken cancellationToken)
{
return new EntityQueryResult<TEntity>(
await ExecuteCoreAsync(cancellationToken).ConfigureAwait(false));
}
/// <summary>
/// Execute query.
/// </summary>
/// <returns>
/// Observable collection of entities and their aspects.
/// </returns>
public async Task<ObservableEntityQueryResult<TEntity>> ExecuteObservableAsync(
CancellationToken cancellationToken)
{
var results = await ExecuteCoreAsync(cancellationToken)
.ConfigureAwait(true); // Return to caller context.
//
// Return an observable result that's tied to the caller's context.
//
return new ObservableEntityQueryResult<TEntity>(
results,
this.context.EventQueue);
}
/// <summary>
/// Builder class for queries.
/// </summary>
public class Builder
{
private readonly EntityContext context;
internal Builder(EntityContext context)
{
this.context = context;
}
/// <summary>
/// Query entities by performing a search.
/// </summary>
public EntityQuery<TEntity> Search<TQuery>(TQuery query)
{
return new EntityQuery<TEntity>(
this.context,
ct => this.context.SearchAsync<TQuery, TEntity>(query, ct));
}
/// <summary>
/// Query entities by performing a wildcard search.
/// </summary>
public EntityQuery<TEntity> List()
{
return Search(WildcardQuery.Instance);
}
/// <summary>
/// Query a single entity.
/// </summary>
public EntityQuery<TEntity> Get(ILocator locator)
{
return new EntityQuery<TEntity>(
this.context,
async ct => Lists.FromNullable(await this.context
.QueryAspectAsync<TEntity>(locator, ct)
.ConfigureAwait(false)));
}
/// <summary>
/// Query entities by parent locator.
/// </summary>
/// <param name="locator">Locator of parent entity</param>
public EntityQuery<TEntity> ByAncestor(ILocator locator)
{
return new EntityQuery<TEntity>(
this.context,
ct => this.context.ListDescendantsAsync<TEntity>(locator, ct));
}
}
}
}