Skip to main content

Languages, markets and channels

TODO

Explain how values vary by context (for example a name per language), and how entities belong to contexts (for example the products in the web shop).

Calculated context memberships​

Vox determines candidate contexts separately for every entity/context type pair. If inheriting relation paths exist, only the contexts reached through those relations are candidates. If a membership calculation exists without a relation path, every stored entity of the context type is a candidate. There can be one IVoxContextMembershipCalculation<TEntity, TContext> for each pair.

This makes it possible to use relations for one context and calculations alone for another. For example, products can have explicit web/app channel relations while their market memberships are inferred from price and inventory data:

public class ProductMarketMembership : IVoxContextMembershipCalculation<Product, Market>
{
public Task<bool> IsMemberAsync(
Product product,
Market market,
IVoxCalculationContext context,
CancellationToken cancellationToken)
{
return Task.FromResult(
product.PriceMarketIds.Contains(market.Id) &&
product.InventoryMarketIds.Contains(market.Id));
}
}

Because Product has no inheriting relation path to Market, Vox tests every stored market. Creating, updating or deleting a market triggers these calculated-only memberships again. If a Product-to-Market path is later added, Vox automatically switches the pair to relation-derived candidates and the same calculation narrows that candidate set.

The next example assumes a relation-derived product/market membership and keeps it only while the product is published. RecalculateAt records the boundaries at which the answer may change; Vox schedules durable recalculations even when the current result did not change.

public class PublishedProductMarketMembership(TimeProvider timeProvider)
: IVoxContextMembershipCalculation<Product, Market>
{
public Task<bool> IsMemberAsync(
Product product,
Market market,
IVoxCalculationContext context,
CancellationToken cancellationToken)
{
if (product.PublishedFrom is { } publishedFrom)
{
context.RecalculateAt(publishedFrom);
}

if (product.PublishedTo is { } publishedTo)
{
context.RecalculateAt(publishedTo);
}

var now = timeProvider.GetUtcNow().UtcDateTime;
return Task.FromResult(
(product.PublishedFrom == null || product.PublishedFrom <= now) &&
(product.PublishedTo == null || now < product.PublishedTo));
}
}

Membership calculations can also depend on the stored memberships of related entities. The batch read records a dependency for every supplied entity, including those with no current membership, so creating or removing a membership later triggers the calculation again.

public class BundleMarketMembership : IVoxContextMembershipCalculation<Bundle, Market>
{
public async Task<bool> IsMemberAsync(
Bundle bundle,
Market market,
IVoxCalculationContext context,
CancellationToken cancellationToken)
{
var products = await context.LoadRelatedAsync<Product>(bundle, cancellationToken);
var marketIdsByProduct = await context.GetContextIdsAsync<Market>(products, cancellationToken);
return products.Any(product => marketIdsByProduct[product.Id].Contains(market.Id));
}
}

RecalculateAt is available to ordinary calculated-property calculations as well. Local DateTime values are converted to UTC, unspecified values are treated as UTC, past instants are ignored, and only the earliest future instant requested by a run is retained.