Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/building/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,38 @@ The layout of files should look like this:
```

Lastly, run `nuget pack .` from within the folder. Note that you may need the latest `nuget.exe`.

## Optimized provider instance metadata

For the design rationale, accessor contract, binding and promotion flow, identity rules, and
validation entry points, see [Provider instance metadata](provider-instance-metadata.md).

When the `PSOptimizedProviderObjects` experimental feature is enabled, built-in providers
implementing `IProviderItemObjectFactory` can supply compact item wrappers. FileSystem
output for `FileInfo` and `DirectoryInfo` stores its drive and,
on Unix, stat metadata inline in a closed generic `PSObject<TMetadata>`. Static
accessors declared on the metadata type describe member names and order once per
closed type. Direct instance-member reads derive paths and container status from the
current `FileInfo` or `DirectoryInfo`, without creating `PSNoteProperty` objects.
As a result, unmaterialized path metadata follows filesystem operations such as
`FileInfo.MoveTo`; once member enumeration or mutation promotes the metadata to
ordinary note properties, those values remain a snapshot.
Member enumeration, matching, direct inspection, and mutation promote the source to
ordinary note properties in the normal ordered dictionary.

Instance-member storage continues to use the existing resurrection table, so unwrapping
and rewrapping a provider result preserves its metadata source.

Eligible scalar FileSystem `Get-Content` string and byte outputs use the same mechanism,
with an inline `ReadCount` and a reference to a shared source-path cache. Unlike item
metadata, content paths are snapshots. String outputs own metadata through wrapper
identity; byte outputs use the boxed byte object's reference identity, so equal byte values
in distinct boxes carry independent counts and rewrapping the same box recovers its metadata.

On Windows, eligible Registry key output uses `PSObject<RegistryProviderMetadata>`.
The key supplies its value names and path, while the drive supplies the provider and
drive context. Existing instance members and keys without a drive retain the ordinary
note-property output path.

Other providers, batched `IList` output, `-Raw -AsByteStream` byte arrays, and other content types continue to use note properties.
Disabling the feature retains the ordinary note-property paths for compatibility comparisons.
482 changes: 482 additions & 0 deletions docs/building/provider-instance-metadata.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -142,95 +142,76 @@ internal void WriteContentObject(object content, long readCount, PathInfo pathIn
context != null,
"The caller should verify the context.");

if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSOptimizedProviderObjects)
&& pathInfo.Provider.ImplementingType == typeof(FileSystemProvider)
&& (content is string || content is byte)
&& !PSObject.HasInstanceMembers(content, out _))
{
// This specialization covers scalar FileSystem string and byte content.
// Boxed bytes use their box identity as the resurrection-table key, so
// equal byte values in distinct boxes keep independent metadata. Other
// providers and content shapes retain the general note-property path below.
ContentPathsCache cache = EnsureContentPathsCache(pathInfo, context, out _);
uint presenceMask = (cache.HasParentPath ? 1u : 0u)
| (cache.HasChildName ? 2u : 0u)
| (cache.Drive != null ? 4u : 0u);
var metadata = new ContentOutputMetadata { Paths = cache, ReadCount = readCount };
var optimizedResult = new PSObject<ContentOutputMetadata>(content, metadata, presenceMask);
optimizedResult.AttachProviderInstanceMembers(optimizedResult);
Comment thread
powercode marked this conversation as resolved.
Resolved
WriteObject(optimizedResult);
return;
}

PSObject result = PSObject.AsPSObject(content);

Dbg.Diagnostics.Assert(
result != null,
"A PSObject should always be constructed.");

// Use the cached notes if the cache exists and the path is still the same
PSNoteProperty note;

if (_currentContentItem != null &&
((_currentContentItem.PathInfo == pathInfo) ||
string.Equals(
pathInfo.Path,
_currentContentItem.PathInfo.Path,
StringComparison.OrdinalIgnoreCase)))
{
result = _currentContentItem.AttachNotes(result);
}
else
{
// Generate a new cache item and cache the notes

_currentContentItem = new ContentPathsCache(pathInfo);

// Construct a provider qualified path as the Path note
string psPath = pathInfo.Path;
note = new PSNoteProperty("PSPath", psPath);
result.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSPath", psPath);
_currentContentItem.PSPath = psPath;

try
{
// Now get the parent path and child name

string parentPath = null;

if (pathInfo.Drive != null)
{
parentPath = SessionState.Path.ParseParent(pathInfo.Path, pathInfo.Drive.Root, context);
}
else
{
parentPath = SessionState.Path.ParseParent(pathInfo.Path, string.Empty, context);
}

note = new PSNoteProperty("PSParentPath", parentPath);
result.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSParentPath", parentPath);
_currentContentItem.ParentPath = parentPath;

// Get the child name

string childName = SessionState.Path.ParseChildName(pathInfo.Path, context);
note = new PSNoteProperty("PSChildName", childName);
result.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSChildName", childName);
_currentContentItem.ChildName = childName;
}
catch (NotSupportedException)
{
// Ignore. The object just won't have ParentPath or ChildName set.
}

// PSDriveInfo
// Use cached path context when available; otherwise compute it once.
ContentPathsCache paths = EnsureContentPathsCache(pathInfo, context, out bool isNewCache);
result = paths.AttachNotes(result, isNewCache);

if (pathInfo.Drive != null)
{
PSDriveInfo drive = pathInfo.Drive;
note = new PSNoteProperty("PSDrive", drive);
result.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSDrive", drive);
_currentContentItem.Drive = drive;
}
// Add the ReadCount note
PSNoteProperty note = new PSNoteProperty("ReadCount", readCount);
result.Properties.Add(note, true);

// ProviderInfo
WriteObject(result);
}

ProviderInfo provider = pathInfo.Provider;
note = new PSNoteProperty("PSProvider", provider);
result.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSProvider", provider);
_currentContentItem.Provider = provider;
private ContentPathsCache EnsureContentPathsCache(PathInfo pathInfo, CmdletProviderContext context, out bool isNewCache)
{
if (_currentContentItem != null && ((_currentContentItem.PathInfo == pathInfo)
|| (ReferenceEquals(pathInfo.Provider, _currentContentItem.PathInfo.Provider)
&& ReferenceEquals(pathInfo.Drive, _currentContentItem.PathInfo.Drive)
&& string.Equals(pathInfo.Path, _currentContentItem.PathInfo.Path, StringComparison.OrdinalIgnoreCase))))
{
isNewCache = false;
return _currentContentItem;
}

// Add the ReadCount note
note = new PSNoteProperty("ReadCount", readCount);
result.Properties.Add(note, true);
var cache = new ContentPathsCache(pathInfo);
_currentContentItem = cache;
isNewCache = true;
cache.PSPath = pathInfo.Path;
try
{
string parentPath = pathInfo.Drive != null
? SessionState.Path.ParseParent(pathInfo.Path, pathInfo.Drive.Root, context)
: SessionState.Path.ParseParent(pathInfo.Path, string.Empty, context);
cache.ParentPath = parentPath;
cache.HasParentPath = true;
cache.ChildName = SessionState.Path.ParseChildName(pathInfo.Path, context);
cache.HasChildName = true;
}
catch (NotSupportedException)
{
// Match legacy behavior when parsing parent/child paths is unsupported.
}

WriteObject(result);
cache.Drive = pathInfo.Drive;
cache.Provider = pathInfo.Provider;
return cache;
}

/// <summary>
Expand Down Expand Up @@ -287,16 +268,23 @@ public ContentPathsCache(PathInfo pathInfo)
/// </summary>
public string ChildName { get; set; }

public bool HasParentPath { get; set; }

public bool HasChildName { get; set; }

/// <summary>
/// Attaches the cached notes to the specified PSObject.
/// </summary>
/// <param name="content">
/// The PSObject to attached the cached notes to.
/// </param>
/// <param name="omitUnsupportedPaths">
/// Omit unsupported parent and child notes on the first output for a path.
/// </param>
/// <returns>
/// The PSObject that was passed in with the cached notes added.
/// </returns>
public PSObject AttachNotes(PSObject content)
public PSObject AttachNotes(PSObject content, bool omitUnsupportedPaths = false)
{
// Construct a provider qualified path as the Path note

Expand All @@ -306,15 +294,21 @@ public PSObject AttachNotes(PSObject content)

// Now attach the parent path and child name

note = new PSNoteProperty("PSParentPath", ParentPath);
content.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSParentPath", ParentPath);
if (!omitUnsupportedPaths || HasParentPath)
{
note = new PSNoteProperty("PSParentPath", ParentPath);
content.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSParentPath", ParentPath);
}

// Attach the child name

note = new PSNoteProperty("PSChildName", ChildName);
content.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSChildName", ChildName);
if (!omitUnsupportedPaths || HasChildName)
{
note = new PSNoteProperty("PSChildName", ChildName);
content.Properties.Add(note, true);
tracer.WriteLine("Attaching {0} = {1}", "PSChildName", ChildName);
}

// PSDriveInfo

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Management.Automation;

namespace Microsoft.PowerShell.Commands
{
internal struct ContentOutputMetadata
{
// Share the existing per-path cache across emitted lines. A FileInfo alone cannot
// describe provider paths (including PSDrive/content-provider paths), so the cached
// PathInfo context remains the source for the legacy path note values.
internal ContentCommandBase.ContentPathsCache Paths;
internal long ReadCount;

[PSObjectMetadataAccessor("PSPath", 0)]
private static string GetPath(object content, in ContentOutputMetadata metadata) => metadata.Paths.PSPath;

[PSObjectMetadataAccessor("PSParentPath", 1, 1)]
private static string GetParentPath(object content, in ContentOutputMetadata metadata) => metadata.Paths.ParentPath;

[PSObjectMetadataAccessor("PSChildName", 2, 2)]
private static string GetChildName(object content, in ContentOutputMetadata metadata) => metadata.Paths.ChildName;

[PSObjectMetadataAccessor("PSDrive", 3, 4)]
private static PSDriveInfo GetDrive(object content, in ContentOutputMetadata metadata) => metadata.Paths.Drive;

[PSObjectMetadataAccessor("PSProvider", 4)]
private static ProviderInfo GetProvider(object content, in ContentOutputMetadata metadata) => metadata.Paths.Provider;

[PSObjectMetadataAccessor("ReadCount", 5)]
private static long GetReadCount(object content, in ContentOutputMetadata metadata) => metadata.ReadCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ private static void CheckBoolValue(object value, Type boolType)
{
Type resultType = value.GetType();

if (resultType == typeof(PSObject))
resultType = ((PSObject)value).BaseObject.GetType();
if (value is PSObject psObject)
{
resultType = psObject.BaseObject.GetType();
}

if (!(LanguagePrimitives.IsNumeric(resultType.GetTypeCode()) ||
LanguagePrimitives.IsBoolOrSwitchParameterType(resultType)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public class ExperimentalFeature
internal const string PSSerializeJSONLongEnumAsNumber = nameof(PSSerializeJSONLongEnumAsNumber);
internal const string PSProfileDSCResource = "PSProfileDSCResource";

/// <summary>
/// The opt-in feature for lazy instance metadata on provider items and scalar content output.
/// </summary>
internal const string PSOptimizedProviderObjects = nameof(PSOptimizedProviderObjects);

#endregion

#region Instance Members
Expand Down Expand Up @@ -114,6 +119,10 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSProfileDSCResource,
description: "DSC v3 resources for managing PowerShell profile."
),
new ExperimentalFeature(
name: PSOptimizedProviderObjects,
description: "Use inline instance metadata for eligible provider objects."
)
};

Expand Down
26 changes: 22 additions & 4 deletions src/System.Management.Automation/engine/InternalCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,13 @@ private void EndBlockParameterSet()
args: emptyArray);
}

/// <summary>
/// Processes the current input for the ForEach-Object member-name parameter set.
/// </summary>
/// <remarks>
/// Exact provider-property names are checked without materializing notes. Their getters run
/// only after ShouldProcess approves the operation. Wildcard matching still requests member objects.
/// </remarks>
private void ProcessPropertyAndMethodParameterSet()
{
_targetString = string.Format(CultureInfo.InvariantCulture, InternalCommandStrings.ForEachObjectTarget, GetStringRepresentation(InputObject));
Expand Down Expand Up @@ -692,6 +699,7 @@ private void ProcessPropertyAndMethodParameterSet()
}

PSMemberInfo member = null;
string providerPropertyName = null;
if (WildcardPattern.ContainsWildcardCharacters(_propertyOrMethodName))
{
// get the matched member(s)
Expand Down Expand Up @@ -721,7 +729,17 @@ private void ProcessPropertyAndMethodParameterSet()
}
else
{
member = _inputObject.Members[_propertyOrMethodName];
if (_inputObject.ContainsProviderInstanceMember(_propertyOrMethodName))
{
// Keep the source-backed value lazy: resolve it through the normal
// property getter only after ShouldProcess, preserving WhatIf behavior
// and the usual getter error handling without materializing the member.
providerPropertyName = _propertyOrMethodName;
}
else
{
member = _inputObject.Members[_propertyOrMethodName];
}
}

// member is a method
Expand Down Expand Up @@ -781,9 +799,9 @@ private void ProcessPropertyAndMethodParameterSet()
}
else
{
string resolvedPropertyName = null;
string resolvedPropertyName = providerPropertyName;
bool isBlindDynamicAccess = false;
if (member == null)
if (member == null && string.IsNullOrEmpty(providerPropertyName))
{
if ((_inputObject.BaseObject is IDynamicMetaObjectProvider) &&
!WildcardPattern.ContainsWildcardCharacters(_propertyOrMethodName))
Expand Down Expand Up @@ -812,7 +830,7 @@ private void ProcessPropertyAndMethodParameterSet()
{
// member is [presumably] a property (note that it could be a
// dynamic property, if it shows up in GetDynamicMemberNames())
resolvedPropertyName = member.Name;
resolvedPropertyName = member == null ? providerPropertyName : member.Name;
}

if (!string.IsNullOrEmpty(resolvedPropertyName))
Expand Down
Loading
Loading