Tesearis.ArtifactInspectorForUnity.Core 2.0.0

dotnet add package Tesearis.ArtifactInspectorForUnity.Core --version 2.0.0
                    
NuGet\Install-Package Tesearis.ArtifactInspectorForUnity.Core -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tesearis.ArtifactInspectorForUnity.Core" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tesearis.ArtifactInspectorForUnity.Core" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Tesearis.ArtifactInspectorForUnity.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tesearis.ArtifactInspectorForUnity.Core --version 2.0.0
                    
#r "nuget: Tesearis.ArtifactInspectorForUnity.Core, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tesearis.ArtifactInspectorForUnity.Core@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tesearis.ArtifactInspectorForUnity.Core&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Tesearis.ArtifactInspectorForUnity.Core&version=2.0.0
                    
Install as a Cake Tool

Tesearis.ArtifactInspectorForUnity.Core

Typed, lazy access to the objects inside Unity's built artifacts (player builds or asset bundles) from inside the Unity Editor or as a standalone .NET library.

What it does

Tesearis.ArtifactInspectorForUnity.Core mounts a built archive using Unity's own UnityFileSystemApi and exposes the serialized objects inside it. Each object's fields are described by its type tree and read lazily through a TypeTreeReader: nothing is bulk-decoded or copied until you actually ask for a field's value or raw bytes, so inspecting a bundle full of large textures or meshes doesn't require loading them all into memory first.

Typed, tolerant views of specific known object types can be layered on top via the public ArtifactAdapter<T> mechanism (see Custom adapters).

This is the core library: a plain .NET package with no compile-time dependency on Unity. Pairs with the separate Tesearis.ArtifactInspectorForUnity.Editor package (adapters for UnityEngine.* types, etc., see the artifact-inspector-for-unity-editor repository).

Requirements

  • Targets Unity 6000.3+ only: bundles/player builds must be built by Unity 6000.3 or newer, and the UnityFileSystemApi library loaded at runtime must come from a matching Unity Editor install.
  • Targets netstandard2.0 and net8.0. No compile-time dependency on Unity.
  • At runtime, needs a local Unity Editor installation to load UnityFileSystemApi (.dylib / .dll / .so) from. Auto-discovered when running inside the Editor process; outside it, point at the library explicitly (see Running outside the Editor).
  • UnityFileSystemApi is an internal, undocumented Unity component: its location, exported function signatures, and behavior aren't guaranteed across Editor versions, and may change or be removed without notice. Account for that before relying on this in CI or production. ArtifactInspector.GetNativeLibraryVersion() / GetUnityEditorVersion() and SerializedFile.Version let you check what you're actually talking to at runtime (e.g. log or assert on them at startup) instead of only finding out when something breaks.

Install

dotnet add package Tesearis.ArtifactInspectorForUnity.Core

For use from Unity Editor tooling, add the package to an Editor-only assembly (e.g. via NuGetForUnity or by vendoring the DLL). It works equally well as a plain dependency of a standalone .NET tool, as long as ArtifactInspector.SetupLibraryPath is configured.

Quick start

using Tesearis.ArtifactInspectorForUnity.Core;

using var archive = ArtifactInspector.OpenAssetBundle("myasset.bundle");

foreach (var entryName in archive.EntryNames)
{
    using var serializedFile = archive.OpenSerializedFile(entryName);

    foreach (var objectRef in serializedFile.Objects)
    {
        if (objectRef.ClassName != "Texture2D") continue;

        var reader = objectRef.GetReader();
        var name = reader.Field("m_Name").AsString();
        var width = reader.Field("m_Width").AsInt32();
        var height = reader.Field("m_Height").AsInt32();
        Console.WriteLine($"{name}: {width}x{height}");
    }
}

Fields can be nested or array-valued:

var reader = objectRef.GetReader();

if (reader.TryGetField("m_Channels", out var channels))
{
    foreach (var channel in channels.Elements())
    {
        var format = channel.Field("format").AsByte();
    }
}

Loose Player Build files

Opening loose files directly from disk without an archive container (e.g. Player Build output like sharedassets0.assets or globalgamemanagers):

using var serializedFile = ArtifactInspector.OpenSerializedFile("path/to/sharedassets0.assets");

foreach (var objectRef in serializedFile.Objects)
{
    var reader = objectRef.GetReader();
    // ...
}

Repeated and cross-object field lookups (snapshots)

ObjectRef.GetReader() builds a fresh TypeTreeReader every call, which is the right choice for reading an object's fields once. It's the wrong choice for reading the same object's fields repeatedly -- e.g. several sibling MonoBehaviours each resolving their owning GameObject's name -- since each call re-walks that object's type tree from scratch. ObjectRef.Snapshot() fixes that: the first call decodes the object's fields once and caches the result on the owning SerializedFile; every later call for the same object, from any caller, is a plain dictionary lookup with no further reads.

var snapshot = objectRef.Snapshot();
var name = snapshot.Field("m_Name").AsString();

// A later lookup of the same object -- from anywhere -- is served from the cache, not re-walked.
var sameSnapshot = objectRef.Snapshot();

A field whose own size exceeds MaterializeOptions.MaxInlineFieldSizeBytes (1024 bytes by default) is left as a deferred reference instead of eagerly decoded -- large blobs (mesh/texture/audio-scale payloads, unusually long strings/arrays) stay exactly as lazy as GetReader() already makes them; only the fields cheap enough to be worth caching are decoded up front. SnapshotField.IsDeferred reports which; SnapshotField.ToReader() is an explicit escape hatch back to a live, lazy reader for one.

PPtr.TryResolveSnapshot resolves a local reference (e.g. a Component's owning GameObject) straight to a cached snapshot, so chasing the same reference repeatedly costs nothing after the first resolution:

var target = reader.Field("m_GameObject").AsPPtr();
if (target.TryResolveSnapshot(serializedFile, out var gameObject))
{
    Console.WriteLine(gameObject.Field("m_Name").AsString());
}

For a consumer that knows upfront it will touch most or all of a file's objects -- walking a whole scene's hierarchy, say -- SerializedFile.MaterializeAll(options) snapshots every (optionally filtered) object in one pass, sorted by byte offset so the underlying reads stay forward/sequential instead of jumping around. This is an explicit, opt-in call: opening a file never eagerly materializes anything on its own, since doing so for every object in a huge scene could cost several times the file's own size in managed memory.

using Tesearis.ArtifactInspectorForUnity.Core.TypeTree;

// Only GameObject/Transform/MonoBehaviour (ClassIds 1/4/114) -- skips Mesh/Texture2D/AudioClip entirely.
var options = new MaterializeOptions { TypeIdFilter = typeId => typeId is 1 or 4 or 114 };
var result = serializedFile.MaterializeAll(options);
Console.WriteLine($"{result.SucceededCount} objects materialized, {result.Failures.Count} failed");

One object failing to materialize (e.g. an unsupported [SerializeReference] shape, see Known limitations) is recorded in MaterializeResult.Failures, not thrown -- it doesn't abort the rest of the pass.

Custom adapters

Adapters turn a raw type-tree reader into a typed, tolerant view of a known object type, via the public ArtifactAdapter<T> mechanism. Third parties, including Unity Editor-side code living in a separate assembly (where real UnityEngine.* types are available), can register their own adapters and dispatch across all of them with ArtifactAdapterRegistry.Inspect:

using Tesearis.ArtifactInspectorForUnity.Core;
using Tesearis.ArtifactInspectorForUnity.Core.Adapters;

var registry = new ArtifactAdapterRegistry()
    .Register(new AudioClipAdapter())
    .Register(new MonoBehaviourAdapter());

using var archive = ArtifactInspector.OpenAssetBundle("myasset.bundle");

foreach (var obj in registry.Inspect(archive))
{
    switch (obj)
    {
        case AudioClipInfo clip:
            Console.WriteLine($"{clip.Name}: {clip.Channels} channel(s)");
            break;
        case RawObject raw:
            Console.WriteLine($"(unhandled) {raw.ClassName} #{raw.ObjectRef.PathId}");
            break;
    }
}

(AudioClipAdapter and MonoBehaviourAdapter are illustrative here - write your own following Writing an adapter.)

Objects that no registered adapter recognizes come back as a RawObject (its ClassName plus the original ObjectRef) instead of being silently dropped, so you can log them or fall back to raw.ObjectRef.GetReader() yourself.

Inspect is a convenience on top of the manual loop from the quick start above: SerializedFile.Objects/ObjectRef.GetReader() are still there for one-off or lower-level use, and dispatching through a registry additionally reuses a single TypeTreeReader per object across matching and reading, instead of building one for ClassName and another for GetReader(). Inspect can also dispatch directly over loose files via registry.Inspect(serializedFile), and accepts an optional orderByOffset: true flag to sort objects by byte offset for forward-sequential reads.

Writing an adapter

Implement ArtifactAdapter<T>:

public sealed class AudioClipAdapter : ArtifactAdapter<AudioClipInfo>
{
    protected override string ClassName => "AudioClip";

    public override AudioClipInfo Read(ArtifactAdapterContext context)
    {
        var reader = context.Reader; // the same TypeTreeReader ObjectRef.GetReader() would give you
        // ... decode fields, optionally using context.Archive / context.SerializedFile for streamed
        // data or cross-references to other objects in the same file ...
    }
}
  • Override Matches instead of ClassName for anything beyond a straight type-name check, e.g. only handling a MonoBehaviour whose m_Script field (.AsPPtr()) resolves to a particular script.
  • context.SerializedFile.TryGetObject(pathId, out var objectRef) lets an adapter look up another object in the same file by path ID (PPtr resolution).
  • context.Archive lets an adapter read from other archive entries, e.g. to resolve a streamed resource file named by a field in the object.
  • Adapters must not retain context.Reader, context.SerializedFile, or context.Archive past Read returning: copy whatever data you need into your own result type first. Inspect disposes each SerializedFile as it moves on to the next archive entry.
  • Implement IArtifactAdapter directly instead of ArtifactAdapter<T> only if you need a struct result type, or want to skip the ClassName-based default Matches.
  • IArtifactAdapter.ResultType (always typeof(T) on ArtifactAdapter<T>) lets a caller discover what an adapter produces without invoking Read, e.g. to list the types a registry can dispatch to, or to filter which adapters apply before running Inspect over a large archive.

Registries are ordered, first-match-wins: register more specific adapters before more general ones. new ArtifactAdapterRegistry() always starts empty; this library bundles no adapters of its own.

Streamed data

Some object types (textures, meshes, audio clips above a size threshold) don't store their payload inline in the SerializedFile; instead a field names an offset/size into a separate, larger file alongside it (e.g. a texture's .resS streaming data file next to its .assets file). An adapter for such a type reads that field itself and hands the caller a StreamingInfo (Offset, Size, Path) describing where the real bytes live, rather than trying to inline megabytes of pixel/vertex/sample data into its result:

public sealed class Texture2DAdapter : ArtifactAdapter<Texture2DInfo>
{
    protected override string ClassName => "Texture2D";

    public override Texture2DInfo Read(ArtifactAdapterContext context)
    {
        var reader = context.Reader;
        var streamData = reader.Field("m_StreamData");
        var streaming = new StreamingInfo(
            streamData.Field("offset").AsInt64(),
            streamData.Field("size").AsInt32(),
            streamData.Field("path").AsString());

        return new Texture2DInfo(reader.Field("m_Name").AsString(), streaming);
    }
}

// Caller resolves the streamed bytes through the same archive the object came from:
if (!string.IsNullOrEmpty(info.Streaming.Path))
{
    var pixelBytes = info.Streaming.ReadBytes(context.Archive);
}

An empty Path means the payload is inline in the object itself instead (read it directly off reader); this library doesn't decide that for you, since which field means "streamed" and what an empty path means is specific to each Unity type.

Public API

Quick reference; see the examples above for usage, and each type's XML doc comments for full member-level detail.

  • ArtifactInspector: entry point (OpenAssetBundle, OpenSerializedFile, SetupLibraryPath, AddTypeTreeSource/RemoveTypeTreeSource, native/editor version checks).
  • ArtifactArchive, SerializedFile, ObjectRef, ExternalReference, PPtr, ArchiveEntryInfo, IRandomAccessByteSource, GuidFormatting: the archive and serialized file model.
  • TypeTreeReader, TypeTreeNode, TypeTreeSummary: lazy, random access field reading.
  • ObjectSnapshot, SnapshotField, MaterializeOptions, MaterializeResult: cached, mostly-eager field decoding for repeated/cross-object lookups, see Repeated and cross-object field lookups above.
  • ArtifactAdapterRegistry, IArtifactAdapter, ArtifactAdapter<T>, ArtifactAdapterContext, RawObject: the adapter mechanism, see Custom adapters above.
  • StreamingInfo: describes an out-of-line payload (offset/size/path) for adapters covering streamed asset types, with ReadBytes/OpenByteSource helpers for resolving the payload through an ArtifactArchive, see Streamed data above.
  • BinaryFormat.SerializedFileDetector, SerializedFileInfo, StrippedObjectInfo, TypeIdRegistry, YamlSerializedFileDetector: stripped file (no TypeTree) support, see Stripped files below.
  • ArtifactInspectorException, NativeCallException, SerializedFileOpenException, NativeFeatureNotSupportedException, UnsupportedManagedReferenceShapeException: the exception hierarchy, see Exceptions below.

Stripped files (no TypeTree)

Shipped Player builds are normally built with EnableTypeTree=false, which UFS_OpenSerializedFile refuses to open at all. BinaryFormat.SerializedFileDetector reads a SerializedFile's header, object list, and external references directly off bytes instead, with no native call and no TypeTree needed for any of that. It works over the same IRandomAccessByteSource this library already uses internally, so it runs equally well against a bare on-disk file and an archive-mounted entry:

using Tesearis.ArtifactInspectorForUnity.Core.BinaryFormat;

// A bare file on disk, no archive, no Unity Editor process needed:
if (SerializedFileDetector.TryDetect("Builds/StandaloneWindows64/mybundle_Data/sharedassets0.assets", out var info))
{
    Console.WriteLine($"Unity {info.UnityVersion}, EnableTypeTree={info.EnableTypeTree}");
    foreach (var obj in info.Objects)
    {
        Console.WriteLine($"{obj.PathId}: {obj.ClassName} ({obj.ByteSize} bytes)");
    }
}

Or against an archive-mounted entry, including ones the native API can't open:

using var archive = ArtifactInspector.OpenAssetBundle("Builds/StandaloneWindows64/mybundle");
var byteSource = archive.OpenRawByteSource(archive.EntryNames[0]);
SerializedFileDetector.TryDetect(byteSource, out var info);

Field values are out of scope for a stripped file: there is no schema to read them from. What's available is the object list (id/offset/size, labeled by class name via TypeIdRegistry) and the external-reference list, both of which don't actually require a TypeTree to read.

  • BinaryFormat.SerializedFileDetector: TryDetect detects and parses a SerializedFile straight off bytes; IsMissingTypeTrees is a cheap fast path that checks only whether EnableTypeTree is false.
  • SerializedFileInfo (readonly struct): the parsed header, metadata, object list, and external references, plus MetadataParsed/MetadataParseError for versions this library can't fully parse (only format versions 22 and 23, Unity 6000.3.x, are supported).
  • StrippedObjectInfo (readonly struct): PathId, TypeId, ByteOffset, ByteSize, ClassName; deliberately separate from ObjectRef since there's no TypeTree to build a reader from, so it exposes no GetReader(). Name is also populated best-effort, with no TypeTree, for classes where m_Name's byte position is known without one: direct NamedObject subtypes (Texture2D, Mesh, AudioClip, Material, ...), where it's reliably the object's first field, and MonoBehaviour, at a fixed offset past its PPtr/enabled prefix (see BinaryFormat.StrippedObjectNameReader). null for every other class, and for a read that didn't look like a valid name -- this is deliberately not a general field decoder.
  • TypeIdRegistry: static Unity ClassID to class name lookup, the only way to label a stripped object's type.
  • YamlSerializedFileDetector: sniffs the %YAML 1.1 magic to reject Editor-text-format .asset/.prefab/.unity files up front, as a fast triage before attempting a binary parse.

ArtifactArchive.OpenSerializedFile also uses this detector internally: when the native open fails and the entry's bytes positively confirm it has no TypeTrees, it throws SerializedFileOpenException instead of the generic NativeCallException: see Exceptions.

Running outside the Editor

Inside the Editor process, the native UnityFileSystemApi library is located automatically from the running instance (via EditorApplication.applicationPath) and calls are made against that instance's own bundled copy, with no setup call needed.

Outside the Editor (a standalone tool, a CI job, a test runner) there is no running Editor process to auto-detect one from. In that case, ArtifactInspector.SetupLibraryPath(path) must be called once, before any other call into the library, otherwise those calls fail.

using Tesearis.ArtifactInspectorForUnity.Core;

// Pass exact location of UnityFileSystemApi (.dylib / .dll / .so).
ArtifactInspector.SetupLibraryPath("/path/to/UnityFileSystemApi.dylib");

using var archive = ArtifactInspector.OpenAssetBundle("Builds/StandaloneWindows64/mybundle");
  • ArtifactInspector.SetupLibraryPath(string path): points the library at an explicit UnityFileSystemApi native library path, for use when there is no running Editor process to auto-detect one from.

Newer SerializedFile formats (version ≥ 23) can have their TypeTree blobs extracted out-of-band at build time instead of stored inline; opening one of those requires registering that external source first, via ArtifactInspector.AddTypeTreeSource(path). Like SetupLibraryPath, this is a process-wide setup step, called once before opening a file that needs it, not per-archive/per-file. It requires a native library new enough to support it (Unity 6.5+); on an older library it throws NativeFeatureNotSupportedException instead of failing silently or crashing.

ArtifactInspector.AddTypeTreeSource("/path/to/extracted/typetree.dat");

using var archive = ArtifactInspector.OpenAssetBundle("Builds/StandaloneWindows64/mybundle");
  • ArtifactInspector.AddTypeTreeSource(string filePath) / RemoveTypeTreeSource(string filePath): register or unregister an out-of-band TypeTree source, process-wide.

Exceptions

  • ArtifactInspectorException: base type for exceptions raised by this library.
  • NativeCallException : ArtifactInspectorException: a call into the native UnityFileSystemApi failed; carries the underlying ReturnCode.
  • SerializedFileOpenException : ArtifactInspectorException: thrown by ArtifactArchive.OpenSerializedFile in place of NativeCallException specifically when the failure is positively confirmed (via SerializedFileDetector.IsMissingTypeTrees) to be caused by the entry having no TypeTrees; carries EntryName and MissingTypeTrees. Every other OpenSerializedFile failure mode still throws NativeCallException unchanged.
  • NativeFeatureNotSupportedException : ArtifactInspectorException: thrown in place of NativeCallException when a call requires a native entry point that isn't present at all in the loaded UnityFileSystemApi library (e.g. AddTypeTreeSource/RemoveTypeTreeSource against a native library older than Unity 6.5, or SerializedFile.TypeTrees/GetTypeTreeByIndex against a native library that doesn't export UFS_GetTypeTreeCount/UFS_GetTypeTreeInfo/UFS_GetTypeTreeByIndex); carries SymbolName.
  • TypeTree.UnsupportedManagedReferenceShapeException : ArtifactInspectorException: thrown when a [SerializeReference] polymorphic field's offset or size is actually needed, e.g. reading that field directly, or a sibling positioned after it whose offset can't be resolved without it, see Known limitations below; carries Name and TypeName. Merely containing such a field elsewhere in an object's schema doesn't throw: ObjectRef.GetReader() still succeeds, and every other field remains readable. ArtifactAdapterRegistry.Adapt/Inspect catch this and fall back to a RawObject instead of propagating it.

Resource management

ArtifactArchive and SerializedFile both implement IDisposable. Dispose in nested order (innermost first), as in the quick start example above. Disposing an archive also actively invalidates any SerializedFiles still open from it: any further call on one throws ObjectDisposedException, the same as if you'd disposed that SerializedFile directly, rather than leaving it looking usable while its native calls silently target a mount that no longer exists.

Testing

dotnet test runs the full suite, but the integration tests under tests/Tesearis.ArtifactInspectorForUnity.Core.Tests/Native/Interop/ that exercise the real native UnityFileSystemApi library are skipped automatically unless the binary for the current OS is present.

To run them against a specific Unity Editor version, copy that Editor install's UnityFileSystemApi.dylib (macOS) / .dll (Windows) / .so (Linux) into tests/Tesearis.ArtifactInspectorForUnity.Core.Tests/UnityFileSystemApiLibraries/. These are Unity's own undocumented native components and must not be redistributed.

The tests under tests/Tesearis.ArtifactInspectorForUnity.Core.Tests/Model/RealTestDataIntegrationTests.cs go one step further and exercise the full pipeline, mounting an archive, walking its objects, and dispatching them through the adapter registry, against real Unity-built output. They're skipped the same way unless there's also something to run them against: drop any real file (an asset bundle, a player build's data file, ...) into tests/Tesearis.ArtifactInspectorForUnity.Core.Tests/TestData/. No particular name, extension, or content is expected: every file found there is exercised, and the assertions are structural (it opens, every object's fields are readable, nothing is silently dropped) rather than tied to specific expected values, so any real data works. Neither of these folders' contents are committed (see their .gitkeeps) or redistributed.

Known limitations

A few boundaries are intentional, not oversights:

  • [SerializeReference] fields aren't decoded. Unity's polymorphic managed-reference shapes aren't readable via the type-tree walk this library uses; reading one such field (or a sibling positioned after it) throws UnsupportedManagedReferenceShapeException instead of misreading it -- see Exceptions. Every other field remains readable.
  • No WebGL bundle support. Unity WebGL's separate UnityWebData1.0 container format isn't handled; this is new scope, not a gap in existing functionality.

Reading field-level data through an archive-mounted entry (ArtifactArchive.ReadRawEntry/OpenRawByteSource, or walking a TypeTreeReader's fields via HasField/Field) was previously suspected to crash the host process (a native segfault) for some entries, based on a standalone .NET host against real Addressables/AssetBundle output. It hasn't reproduced across multiple builds run inside an actual Unity Editor process since, though the root cause was never isolated. If you hit a crash while reading archive-mounted entry data, please open an issue with repro details.

See CHANGELOG.md for what's included in each release.

Acknowledgments

This library's approach to the UnityFileSystemApi native binding and type-tree reading was informed by studying UnityDataTools as a reference for the shape of Unity's undocumented native ABI and serialized-file layout, then independently written from scratch. One current exception: BinaryFormat/TypeIdRegistry.cs's ClassID lookup table is sourced from UnityDataTools' own generated table and used here under its license rather than this project's own. See ThirdPartyNotices.md for the full breakdown.

"Unity" and "Unity Technologies" are trademarks of Unity Technologies. This project is not affiliated with, endorsed by, or sponsored by Unity Technologies.

License

MIT, see LICENSE, with exceptions noted in ThirdPartyNotices.md.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 88 9/22/2026
1.0.0 114 8/30/2026