Use case: Querying metadata
Before you start
Before you start, install the following Unity Cloud package dependency from the registry by name:
unity.cloud.identity
For more information on how to install packages by name, see Installing a package by name
How do I...?
Upload an asset
To upload an asset, follow these steps
Set up a IMetadataRepository
public IMetadataRepository CreateMetadataRepository(IDataset dataset, IServiceHttpClient serviceHttpClient, IServiceHostResolver serviceHostResolver)
{
var factory = new MetadataRepositoryFactory();
return factory.Create(dataset, serviceHttpClient, serviceHostResolver);
}
IMetadataRepository
is the first entry point for any metadata query.
The constructor of the default implementation requires:
- An instance of
IServiceHttpClient
andIServiceHostResolver
. See Best practices: dependency injection page of the Identity package documentation for more information. - A
projectId
,assetId
anddatasetId
. See Unity Cloud Common documentation for more information.
Query metadata
public async Task QueryingMetadata(IMetadataRepository metadataRepository)
{
// Perform the query.
var request = metadataRepository.Query();
// Iterate through the query result.
await foreach (var each in request)
{
var id = each.Id;
var name = each.Name;
var hasChildren = each.HasChildren;
var ancestors = each.AncestorIds;
var rootParent = ancestors[0];
var directParent = ancestors[^1];
// Each entry in the result is attached to a single ownerId, and contains key / values associated with it.
foreach (var key in each.Properties.Keys)
{
var value = each.Properties[key];
}
}
}
With a IMetadataRepository
, you can perform a query by combining the Query
and iterate its result.
For each item part of the IAsyncEnumerable
, you can access data through the MetadataInstance
object. This object contains these properties:
Id
is the unique identifier associated with the metadata.Properties
expose the queried metadata associated with theId
.Name
is the name of the instance as it is in the source file.HasChildren
is a boolean indicating if the instance has children or is a leaf.AncestorIds
is a list of the ancestor ids of the instance where the first element is the top level parent (root) of the instance and the last element is the direct parent.
Use filters to build advanced queries
public async Task QueryingMetadataAdvanced(IMetadataRepository metadataRepository, IEnumerable<string> topLevelFieldsToQuery, InstanceId[] ownerIdsToConsider, string fieldFilterKey, string expectedValue)
{
var request = metadataRepository
.Query()
.Select(new MetadataPathCollection(topLevelFieldsToQuery))
.WhereInstanceEquals(ownerIdsToConsider)
.WhereFieldEquals(fieldFilterKey, expectedValue);
await foreach (var metadata in request)
{
The query described in the section above is the most standard query that you could expect, as it doesn't use any filter. Following a LINQ-like approach, the query system provides a way to build more advanced queries.
SELECT filter, to query a subset of the metadata
public async Task QueryingMetadataWithSelectFilter(IMetadataRepository metadataRepository, IEnumerable<string> rootPathsToQuery)
{
// Query will return matches whose metadata content is restricted to the specified paths
var request = metadataRepository
.Query()
.Select(new MetadataPathCollection(rootPathsToQuery));
await foreach (var metadata in request)
{
Using the Select
method, you can specialize the query to ensure that only a specific set of root keys will be included in the response.
Note
This will also forcefully add the key to every fetched metadata. If it didn't originally contain it, it will appear as empty.
SELECT-ALL filter, to query everything for each instance
public async Task QueryingMetadataWithSelectAllFilter(IMetadataRepository metadataRepository)
{
// Query will return matches whose metadata content is everything
var request = metadataRepository
.Query()
.Select(MetadataPathCollection.All);
await foreach (var metadata in request)
{
Using the Select
method with MetadataPathCollection.All
, all the available data for every instance will be included in the response.
SELECT-ONLY-ID filter, to query no metadata content
public async Task QueryingMetadataWithSelectOnlyIdFilter(IMetadataRepository metadataRepository)
{
// Query will return matches without metadata content, simply the id
var request = metadataRepository
.Query()
.Select(MetadataPathCollection.None, new OptionalData(OptionalData.Fields.Id));
var ids = new List<InstanceId>();
await foreach (var metadata in request)
ids.Add(metadata.Id);
}
Using the Select
method with MetadataPathCollection.None
, no metadata will be returned. The result will only contain the ids. This is especially useful when used with a Where
filter.
WHERE-INSTANCE-EQUALS filter, to query a subset of dataset instances
public async Task QueryingMetadataWithWhereInstanceEqualsFilter(IMetadataRepository metadataRepository, IEnumerable<InstanceId> ownerIdsToConsider)
{
// Query will only return matches that are attached to the specified owner Ids
var request = metadataRepository
.Query()
.WhereInstanceEquals(ownerIdsToConsider);
await foreach (var metadata in request)
{
Using the WhereInstanceEquals
method, you can specialize the query to ensure that only a specific subset of instances in the dataset will be included in the response.
WHERE filter, to apply a metadata-based condition
public async Task QueryingMetadataWithWhereFilter(IMetadataRepository metadataRepository, string fieldKey, string expectedValue)
{
// Query will only contain matches that follow the specified criterium
var request = metadataRepository
.Query()
.WhereFieldEquals(fieldKey, expectedValue);
await foreach (var metadata in request)
{
Using the WhereKeyEquals
method, you can specialize the query to ensure that the matches to be included in the response follow a specific metadata-based condition:
- Metadata should contain a specific key.
- This key should have a value exactly equal to a constant value.
- Requesting the null value will search for metadata values that equals to null or an empty string.
public async Task QueryingMetadataWithWhereNotFilter(IMetadataRepository metadataRepository, string fieldKey)
{
// Query will only contain matches that follow the specified criterium
var request = metadataRepository
.Query()
.WhereFieldNotEquals(fieldKey, null);
await foreach (var metadata in request)
{
Using the WhereKeyNotEquals
method, you can specialize the query to ensure that the matches to be included in the response follow a specific negative metadata-based condition:
- Metadata should contain a specific key.
- This key should have a value not equal to a constant value.
Combine filters
public async Task QueryingMetadataAdvanced(IMetadataRepository metadataRepository, IEnumerable<string> topLevelFieldsToQuery, InstanceId[] ownerIdsToConsider, string fieldFilterKey, string expectedValue)
{
var request = metadataRepository
.Query()
.Select(new MetadataPathCollection(topLevelFieldsToQuery))
.WhereInstanceEquals(ownerIdsToConsider)
.WhereFieldEquals(fieldFilterKey, expectedValue);
await foreach (var metadata in request)
{
You can use any combination of the filters described above to build advanced queries and apply multiple filters at once.
Use the result
IAsyncEnumerable
public async Task UsingTheEnumerableReturnedFromAQuery(IMetadataRepository metadataRepository, string fieldKey)
{
var request = metadataRepository.Query();
await foreach (var metadata in request)
{
var id = metadata.Id;
var name = metadata.Name;
var ancestors = metadata.AncestorIds;
var hasChildren = metadata.HasChildren;
var properties = metadata.Properties[fieldKey];
}
}
Since the result is an IAsyncEnumerable
, you can use the await foreach
statement to iterate through the matches.
MetadataObjects and MetadataValues
public async Task UsingMetadataObjects(IMetadataRepository metadataRepository, InstanceId id, string fieldKey, string subKey)
{
var request = metadataRepository.Query();
await foreach (var metadata in request)
{
//MetadataContainer can either be MetadataObject, MetadataArray or MetadataValues
var container = metadata.Properties[fieldKey];
}
}
public async Task GettingAMetadataValue(IMetadataRepository metadataRepository, InstanceId id, string rootKey)
{
var request = metadataRepository.Query();
await foreach (var metadata in request)
{
var value = metadata.Properties[rootKey].ToString();
}
}
The Metadata is always returned as a MetadataObject as it always is a valid JSON object at the root. MetadataObject extends MetadataContainer that is also extended by MetadataValue and MetadataArray.
You can use a MetadataObject as a IReadOnlyDictionary and access any valid JSON root objects, values or arrays by using the square bracket operator. When trying to read a value you can either use ToString()
or ToNumber()
depending on the underlying type.
Hierarchy of instances
Get the parents hierarchy of an instance
public async Task QueryingMetadataWithParentHierarchy(IMetadataRepository metadataRepository, InstanceId id)
{
var instance = await metadataRepository.Query().WhereInstanceEquals(id).GetFirstOrDefaultAsync(default);
var ancestorIds = instance.AncestorIds;
var rootParentId = ancestorIds[0];
var directParentId = ancestorIds[^1];
var ancestors = await metadataRepository.Query().WhereInstanceEquals(ancestorIds).ToDictionaryAsync(x => x.Id, x => x, default);
var rootParent = ancestors[rootParentId];
var directParent = ancestors[directParentId];
}
Each instance has a parent hierarchy that can be accessed through the AncestorIds
property. This property is a list of the ancestor ids of the instance where the first element is the top level parent (root) of the instance and the last element is the direct parent.
Get the children of an instance
public async Task QueryingDirectChildren(IMetadataRepository metadataRepository, InstanceId id)
{
var instance = await metadataRepository.Query().WhereInstanceEquals(id).GetFirstOrDefaultAsync(default);
if (instance.HasChildren)
{
var directChildren = metadataRepository.Query().WhereHasAncestor(id, 0);
}
}
By filtering the query with the WhereAncestorEquals
method, you can specialize the query to ensure that only the direct children of a specific instance will be included in the response by setting the max depth value to 0.
public async Task QueryingAllChildren(IMetadataRepository metadataRepository, InstanceId id)
{
var instance = await metadataRepository.Query().WhereInstanceEquals(id).GetFirstOrDefaultAsync(default);
if (instance.HasChildren)
{
var allChildrenAndSubChildren = metadataRepository.Query().WhereHasAncestor(id, int.MaxValue);
}
}
If you want to get all the children of a specific instance, you can use the WhereAncestorEquals
method with a higher max depth value.
- If you set the depth to 0, you will get the direct children of the instance.
- If you set the depth to 1, you will get the direct children and the children of the children of the instance.
- If you set the depth to
int.MaxValue
, you will get all the instances which has the given instance id as part of itsAncestorIds
list.
Use helper methods
Getting all keys in the dataset
public async Task QueryingAllRootPaths(IMetadataRepository metadataRepository)
{
// The result will contain all the root paths in the dataset
var result = await metadataRepository.GetAllPathsAsync();
}
By using the GetAllPathsAsync
method, you can retrieve all the root keys that are present in the dataset.
Note
This method can have lower performance if the dataset contains too much content.
Getting all owner ids in the dataset
public async Task QueryingAllIds(IMetadataRepository metadataRepository, CancellationToken cancellationToken)
{
// The result will contain all the owner ids in the dataset
var request = metadataRepository.Query().Select(MetadataPathCollection.None).WithCancellation(cancellationToken);
await foreach (var instance in request)
{
var id = instance.Id;
}
}
By using the GetAllIdsAsync
method, you can retrieve all the owner ids present in the dataset.