Grain placement filters
Placement first determines which silos are compatible with a grain type. Filters then reduce that candidate set, in order, before the grain’s placement strategy selects a target.
Use filters for constraints and preferences such as availability zone, hardware capability, or deployment tier. Don’t use them to encode per-request business routing that would make one grain identity behave like multiple independent entities.
Configure silo metadata
Section titled “Configure silo metadata”Built-in filters compare silo metadata on the calling silo with metadata on candidate silos. Configure consistent keys and values on every participating silo:
siloBuilder.UseSiloMetadata( new Dictionary<string, string> { ["zone"] = "west-1", ["tier"] = "premium" });External clients don’t have silo metadata. Design filtered grain activation paths so the initiating placement request comes from a silo with the required metadata.
Require metadata matches
Section titled “Require metadata matches”RequiredMatchSiloMetadataPlacementFilterAttribute keeps only candidates matching every configured key:
#pragma warning disable ORLEANSEXP004[RequiredMatchSiloMetadataPlacementFilter( ["zone", "tier"])]public sealed class PremiumZoneGrain : Grain, IPremiumZoneGrain{}#pragma warning restore ORLEANSEXP004Placement fails if no compatible silo matches. Use this filter only for hard requirements.
Prefer metadata matches
Section titled “Prefer metadata matches”PreferredMatchSiloMetadataPlacementFilterAttribute prefers candidates matching the ordered keys and progressively falls back by dropping earlier keys:
#pragma warning disable ORLEANSEXP004[PreferredMatchSiloMetadataPlacementFilter( ["rack", "zone"], minCandidates: 2)]public sealed class LocalityGrain : Grain, ILocalityGrain{}#pragma warning restore ORLEANSEXP004minCandidates prevents a narrow preference from concentrating placements on too few silos. Its default is 2.
Combine and order filters
Section titled “Combine and order filters”When a grain class has multiple filters, give each a unique order value. Orleans applies lower values first. Attribute declaration order isn’t a reliable ordering mechanism.
#pragma warning disable ORLEANSEXP004[RequiredMatchSiloMetadataPlacementFilter( ["tier"], order: 0)][PreferredMatchSiloMetadataPlacementFilter( ["rack", "zone"], minCandidates: 2, order: 10)]public sealed class OrderedFilterGrain : Grain, IOrderedFilterGrain{}#pragma warning restore ORLEANSEXP004The placement strategy sees only candidates that remain after every filter. A preferred filter can broaden only within the candidate set it receives; it can’t restore candidates removed by an earlier required filter.
Read metadata from a grain
Section titled “Read metadata from a grain”Inject ISiloMetadataCache and IGrainRuntime when grain logic needs silo metadata. Use IGrainRuntime.SiloAddress to identify the current activation’s silo. Metadata reads don’t influence placement retroactively.
Implement a custom filter
Section titled “Implement a custom filter”Prefer the built-in filters when exact metadata matching is sufficient. A custom filter is useful for a policy with different semantics, such as a numeric threshold. It has three parts:
- A PlacementFilterStrategy that carries configuration and a unique order.
- A PlacementFilterAttribute that attaches the strategy to a grain class.
- An IPlacementFilterDirector that returns a subset of the candidate SiloAddress values.
The following example requires candidates to advertise a minimum logical core count in the hardware.cores silo metadata entry. First, define the attribute and strategy:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]public sealed class MinimumSiloCoresPlacementFilterAttribute( int minimumCores, int order = 0) : PlacementFilterAttribute( new MinimumSiloCoresPlacementFilterStrategy(minimumCores, order));
public sealed class MinimumSiloCoresPlacementFilterStrategy( int minimumCores, int order) : PlacementFilterStrategy(order){ private const string MinimumCoresProperty = "minimum-cores";
public MinimumSiloCoresPlacementFilterStrategy() : this(1, 0) { }
public int MinimumCores { get; private set; } = ValidateMinimumCores(minimumCores);
public override void AdditionalInitialize(GrainProperties properties) { var value = GetPlacementFilterGrainProperty( MinimumCoresProperty, properties);
if (!int.TryParse( value, NumberStyles.None, CultureInfo.InvariantCulture, out var parsedValue)) { throw new ArgumentException( $"Invalid {MinimumCoresProperty} property value."); }
MinimumCores = ValidateMinimumCores(parsedValue); }
protected override IEnumerable<KeyValuePair<string, string>> GetAdditionalGrainProperties( IServiceProvider services, Type grainClass, GrainType grainType, IReadOnlyDictionary<string, string> existingProperties) { yield return new( MinimumCoresProperty, MinimumCores.ToString(CultureInfo.InvariantCulture)); }
private static int ValidateMinimumCores(int value) => value > 0 ? value : throw new ArgumentOutOfRangeException( nameof(value), "The minimum core count must be positive.");}Filter configuration is stored in the grain manifest rather than serialized with the attribute instance. The public parameterless constructor lets dependency injection create the strategy. GetAdditionalGrainProperties writes configuration to the manifest, and AdditionalInitialize restores and validates it on each silo.
Next, implement the director:
public sealed class MinimumSiloCoresPlacementFilterDirector( ISiloMetadataCache siloMetadataCache) : IPlacementFilterDirector{ private const string SiloCoresMetadataKey = "hardware.cores";
public IEnumerable<SiloAddress> Filter( PlacementFilterStrategy filterStrategy, PlacementTarget target, IEnumerable<SiloAddress> silos) { if (filterStrategy is not MinimumSiloCoresPlacementFilterStrategy strategy) { throw new ArgumentException( $"Expected {nameof(MinimumSiloCoresPlacementFilterStrategy)}.", nameof(filterStrategy)); }
return silos.Where(silo => { var metadata = siloMetadataCache.GetSiloMetadata(silo).Metadata; return metadata.TryGetValue( SiloCoresMetadataKey, out var value) && int.TryParse( value, NumberStyles.None, CultureInfo.InvariantCulture, out var coreCount) && coreCount >= strategy.MinimumCores; }); }}The director excludes candidates with missing, malformed, or insufficient metadata. If none remain, placement fails instead of silently weakening the requirement. A preference filter should explicitly return an appropriate fallback subset when its preferred result is empty.
Apply the filter to a grain class. A placement strategy still chooses from the candidates which remain:
[MinimumSiloCoresPlacementFilter(minimumCores: 16)][ResourceOptimizedPlacement]public sealed class ComputeGrain : Grain, IComputeGrain{ public Task Ping() => Task.CompletedTask;}Finally, register the filter on every silo:
public static void AddCustomPlacementFilter(ISiloBuilder siloBuilder){ siloBuilder.Services.AddPlacementFilter< MinimumSiloCoresPlacementFilterStrategy, MinimumSiloCoresPlacementFilterDirector>( ServiceLifetime.Transient);}AddPlacementFilter requires a ServiceLifetime for the strategy. This example uses Transient because initialization mutates the strategy with grain-type-specific configuration. Orleans caches the resulting strategy per grain type. The director is always registered as a keyed singleton, regardless of the strategy lifetime, so it must be thread-safe and use singleton-safe dependencies.
Return only candidates from the input sequence, keep filtering fast and deterministic for the supplied data, and monitor placement failures caused by hard requirements. Silo metadata is operator-provided scheduling information, not live utilization data or a security boundary. Use resource-optimized placement for live load signals and enforce authorization independently.
During placement, read application request metadata from PlacementTarget.RequestContextData; the static RequestContext isn’t populated because no activation exists yet.
