File: PackageIndex.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.Packaging.csproj (Microsoft.DotNet.Build.Tasks.Packaging)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NuGet.Frameworks;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Collections;
using Newtonsoft.Json.Linq;
using System.Xml.Linq;

namespace Microsoft.DotNet.Build.Tasks.Packaging;

public class PackageIndex
{
    static JsonSerializer s_serializer = CreateSerializer();

    static ConcurrentDictionary<string, PackageIndex> s_indexCache = new ConcurrentDictionary<string, PackageIndex>();

    public SortedDictionary<string, PackageInfo> Packages { get; set; } = new SortedDictionary<string, PackageInfo>();

    public SortedDictionary<string, string> ModulesToPackages { get; set; } = new SortedDictionary<string, string>();

    public MetaPackages MetaPackages { get; set; } = new MetaPackages();

    public string PreRelease { get; set; }

    [JsonIgnore]
    public HashSet<string> IndexSources { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    public static PackageIndex Load(IEnumerable<string> packageIndexFiles)
    {
        string indexKey = String.Join("|",
            packageIndexFiles.Select(packageIndexFile => new FileInfo(packageIndexFile))
                             .Select(packageIndexFileInfo => $"{packageIndexFileInfo.FullName}:{packageIndexFileInfo.Length}:{packageIndexFileInfo.LastWriteTimeUtc.Ticks}"));

        PackageIndex result = null;

        if (s_indexCache.TryGetValue(indexKey, out result))
        {
            return result;
        }

        foreach(var packageIndexFile in packageIndexFiles)
        {
            if (result == null)
            {
                result = Load(packageIndexFile);
            }
            else
            {
                result.Merge(packageIndexFile);
            }
        }

        s_indexCache[indexKey] = result;

        return result;
    }

    public static PackageIndex Load(string packageIndexFile)
    {
        using (var file = File.OpenText(packageIndexFile))
        using (var jsonTextReader = new JsonTextReader(file))
        {
            var result = s_serializer.Deserialize<PackageIndex>(jsonTextReader);
            result.IndexSources.Add(Path.GetFullPath(packageIndexFile));
            return result;
        }
    }

    public void Save(string path)
    {
        string directory = Path.GetDirectoryName(path);
        if (!String.IsNullOrEmpty(directory) && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        using (var file = File.CreateText(path))
        {
            s_serializer.Serialize(file, this);
        }
    }

    private static JsonSerializer CreateSerializer()
    {
        var serializer = new JsonSerializer();
        serializer.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;
        serializer.Formatting = Formatting.Indented;
        serializer.NullValueHandling = NullValueHandling.Ignore;
        serializer.DefaultValueHandling = DefaultValueHandling.Ignore;

        serializer.Converters.Add(new VersionConverter());
        serializer.Converters.Add(new InboxFrameworksConverter());
        serializer.Converters.Add(new MetaPackagesConverter());

        return serializer;
    }

    /// <summary>
    /// Merges an index into the currently loaded index if not already merged.
    /// </summary>
    /// <param name="otherIndexFile"></param>
    public void Merge(string otherIndexFile)
    {
        if (!IndexSources.Contains(otherIndexFile))
        {
            Merge(Load(otherIndexFile));
        }
    }

    /// <summary>
    /// Merges a list of indexes into the currently loaded index if not already merged.
    /// </summary>
    /// <param name="otherIndexFiles"></param>
    public void Merge(IEnumerable<string> otherIndexFiles)
    {
        foreach(var otherIndexFile in otherIndexFiles)
        {
            Merge(otherIndexFile);
        }
    }

    /// <summary>
    /// Merges another packageIndex into this package index.  For any overlapping
    /// data 'other' has precedence.
    /// </summary>
    /// <param name="other"></param>
    public void Merge(PackageIndex other)
    {
        if (other.IndexSources.IsSubsetOf(IndexSources))
        {
            return;
        }

        // if pre-release is set on this index and different than the other
        // move pre-release to individual infos
        if (PreRelease != null && !PreRelease.Equals(other.PreRelease))
        {
            foreach(var info in Packages.Values)
            {
                if (info.PreRelease == null)
                {
                    info.PreRelease = PreRelease;
                }
            }

            PreRelease = null;
        }

        foreach(var otherPackage in other.Packages)
        {
            var otherInfo = otherPackage.Value;
            PackageInfo info;

            if (Packages.TryGetValue(otherPackage.Key, out info))
            {
                info.Merge(otherInfo);
            }
            else
            {
                Packages[otherPackage.Key] = info = otherInfo;

                // if pre-release is set on the other index and doesn't match the value of the info, set it
                if (other.PreRelease != null && !other.PreRelease.Equals(info.PreRelease))
                {
                    info.PreRelease = other.PreRelease;
                }
            }
        }

        foreach(var otherModuleToPackage in other.ModulesToPackages)
        {
            ModulesToPackages[otherModuleToPackage.Key] = otherModuleToPackage.Value;
        }

        MetaPackages.Merge(other.MetaPackages);

        foreach(var otherIndexSource in other.IndexSources)
        {
            IndexSources.Add(otherIndexSource);
        }
    }

    public void MergeInboxFromLayout(NuGetFramework framework, string layoutDirectory, bool addPackages = true)
    {
        foreach(var file in Directory.EnumerateFiles(layoutDirectory, "*.dll", SearchOption.AllDirectories))
        {
            var assemblyName = Path.GetFileNameWithoutExtension(file);
            var assemblyVersion = VersionUtility.GetAssemblyVersion(file);

            TryAddInbox(assemblyName, framework, assemblyVersion, addPackages);
        }
    }

    public void MergeFrameworkLists(string frameworkListDirectory, bool addPackages = true)
    {
        foreach (string frameworkDir in Directory.EnumerateDirectories(frameworkListDirectory))
        {
            string targetFrameworkMoniker = Path.GetFileName(frameworkDir);
            NuGetFramework framework = NuGetFramework.Parse(targetFrameworkMoniker);
            foreach (string frameworkListPath in Directory.EnumerateFiles(frameworkDir, "*.xml"))
            {
                MergeFrameworkList(framework, frameworkListPath, addPackages);
            }
        }
    }

    public void MergeFrameworkList(NuGetFramework framework, string frameworkListPath, bool addPackages = true)
    {
        XDocument frameworkList = XDocument.Load(frameworkListPath);
        foreach (var file in frameworkList.Element("FileList").Elements("File"))
        {
            string assemblyName = file.Attribute("AssemblyName").Value;
            var versionAttribute = file.Attribute("Version");
            Version supportedVersion = (versionAttribute != null) ? new Version(versionAttribute.Value) : VersionUtility.MaxVersion;

            TryAddInbox(assemblyName, framework, supportedVersion, addPackages);
        }
    }

    private void TryAddInbox(string assemblyName, NuGetFramework framework, Version version, bool addPackages = true)
    {
        PackageInfo info;
        if (Packages.TryGetValue(assemblyName, out info))
        {
            info.InboxOn.AddInboxVersion(framework, version);
        }
        else if (addPackages)
        {
            Packages[assemblyName] = info = new PackageInfo();
            info.InboxOn.AddInboxVersion(framework, version);
        }
    }

    // helper functions
    public bool TryGetBaseLineVersion(string packageId, out Version baseLineVersion)
    {
        PackageInfo info;
        baseLineVersion = null;

        if (Packages.TryGetValue(packageId, out info))
        {
            baseLineVersion = info.BaselineVersion;
        }

        return baseLineVersion != null;
    }

    public IEnumerable<NuGetFramework> GetAlllInboxFrameworks()
    {
        // very inefficient, included for legacy reasons.
        return Packages.Values.SelectMany(info => info.InboxOn.GetInboxFrameworks()).Distinct().ToArray();
    }

    public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName)
    {
        IEnumerable<NuGetFramework> inboxFrameworks = null;
        PackageInfo info;

        if (Packages.TryGetValue(assemblyName, out info))
        {
            inboxFrameworks = info.InboxOn.GetInboxFrameworks();
        }

        return inboxFrameworks;
    }

    public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName, string assemblyVersionString)
    {
        Version assemblyVersion = FrameworkUtilities.Ensure4PartVersion(assemblyVersionString);

        return GetInboxFrameworks(assemblyName, assemblyVersion);
    }

    public IEnumerable<NuGetFramework> GetInboxFrameworks(string assemblyName, Version assemblyVersion)
    {
        IEnumerable<NuGetFramework> inboxFrameworks = null;
        PackageInfo info;

        if (Packages.TryGetValue(assemblyName, out info))
        {
            inboxFrameworks = info.InboxOn.GetInboxVersions().Where(p => p.Value >= assemblyVersion).Select(p => p.Key);
        }

        return inboxFrameworks;
    }

    public bool IsInbox(string assemblyName, NuGetFramework framework, string assemblyVersionString)
    {
        Version assemblyVersion = FrameworkUtilities.Ensure4PartVersion(assemblyVersionString);

        return IsInbox(assemblyName, framework, assemblyVersion);
    }

    public bool IsInbox(string assemblyName, NuGetFramework framework, Version assemblyVersion)
    {
        PackageInfo info;
        bool isInbox = false;

        if (Packages.TryGetValue(assemblyName, out info))
        {
            isInbox = info.InboxOn.IsInbox(framework, assemblyVersion);
        }

        return isInbox;
    }

    public bool IsStable(string packageId, Version packageVersion)
    {
        PackageInfo info;
        bool isStable = false;

        if (Packages.TryGetValue(packageId, out info))
        {
            isStable = info.StableVersions.Contains(packageVersion);
        }

        return isStable;
    }

    public string GetPreRelease(string packageId)
    {
        PackageInfo info;
        string preRelease = null;

        if (Packages.TryGetValue(packageId, out info))
        {
            preRelease = info.PreRelease;
        }

        if (preRelease == null)
        {
            preRelease = PreRelease;
        }

        return preRelease;
    }

    public Version GetPackageVersionForAssemblyVersion(string packageId, Version assemblyVersion)
    {
        PackageInfo info;
        Version packageVersion = null;

        if (assemblyVersion != null)
        {
            if (Packages.TryGetValue(packageId, out info))
            {
                packageVersion = info.GetPackageVersionForAssemblyVersion(assemblyVersion);
            }
            else
            {
                // if not found assume 1:1 with assembly version
                packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
            }
        }

        return packageVersion;
    }
}

public class PackageInfo
{
    public HashSet<Version> StableVersions { get; set; } = new HashSet<Version>();

    public bool ShouldSerializeStableVersions() { return StableVersions.Count > 0; }

    /// <summary>
    /// Minimum version to use when referencing this package.
    /// </summary>
    public Version BaselineVersion { get; set; }

    /// <summary>
    /// Mapping of frameworks which contain this package inbox (or in a framework package)
    /// </summary>
    public InboxFrameworks InboxOn { get; set; } = new InboxFrameworks();

    public bool ShouldSerializeInboxFrameworkAssemblyVersions() { return InboxOn.Count > 0; }

    /// <summary>
    /// Mapping of assembly version to package version which (first) contains that assembly version.
    /// </summary>
    public SortedDictionary<Version, Version> AssemblyVersionInPackageVersion { get; set; } = new SortedDictionary<Version, Version>();
    
    public bool ShouldSerializeAssemblyVersionInPackageVersion() { return AssemblyVersionInPackageVersion.Count > 0; }

    public string PreRelease { get; set; }

    public void Merge(PackageInfo other)
    {
        StableVersions.UnionWith(other.StableVersions);

        if (other.BaselineVersion != null && BaselineVersion == null)
        {
            BaselineVersion = other.BaselineVersion;
        }

        if (other.PreRelease != null && PreRelease == null)
        {
            PreRelease = other.PreRelease;
        }

        foreach(var inboxOnPair in other.InboxOn.GetInboxVersions())
        {
            InboxOn.AddInboxVersion(inboxOnPair.Key, inboxOnPair.Value);
        }

        foreach (var assemblyVersionInPackage in other.AssemblyVersionInPackageVersion)
        {
            Version otherAssemblyVersion = assemblyVersionInPackage.Key;
            Version otherPackageVersion = assemblyVersionInPackage.Value;

            AddAssemblyVersionInPackage(otherAssemblyVersion, otherPackageVersion);
        }
    }

    public void AddAssemblyVersionsInPackage(IEnumerable<Version> assemblyVersions, Version packageVersion)
    {
        foreach (var assemblyVersion in assemblyVersions)
        {
            AddAssemblyVersionInPackage(assemblyVersion, packageVersion);
        }
    }

    public void AddAssemblyVersionInPackage(Version assemblyVersion, Version packageVersion)
    {
        Version existingPackageVersion;
        if (AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out existingPackageVersion))
        {
            bool existingStable = StableVersions.Contains(existingPackageVersion);
            bool updateStable = StableVersions.Contains(packageVersion);
            
            // always prefer a stable package over unstable package
            // use the highest unstable package version
            // use the lowest stable package version
            if ((updateStable && !existingStable) || // update to stable from unstable
                (updateStable && existingStable && packageVersion < existingPackageVersion) || // update to lower stable
                (!updateStable && !existingStable && packageVersion > existingPackageVersion)) // update to higher non-stable version
            {
                AssemblyVersionInPackageVersion[assemblyVersion] = packageVersion;
            }
        }
        else
        {
            AssemblyVersionInPackageVersion[assemblyVersion] = packageVersion;
        }
    }
    public Version GetPackageVersionForAssemblyVersion(NuGetFramework framework, Version assemblyVersion)
    {
        Version packageVersion = null;

        if (assemblyVersion != null)
        {
            // prefer an explicit mapping
            if (!AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out packageVersion))
            {
                // if not found assume 1:1 with assembly version
                packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
            }
        }

        return packageVersion;
    }

    public Version GetPackageVersionForAssemblyVersion(Version assemblyVersion)
    {
        Version packageVersion = null;

        if (assemblyVersion != null)
        {
            // prefer an explicit mapping
            if (!AssemblyVersionInPackageVersion.TryGetValue(assemblyVersion, out packageVersion))
            {
                // if not found assume 1:1 with assembly version
                packageVersion = VersionUtility.As3PartVersion(assemblyVersion);
            }
        }

        return packageVersion;
    }
}

public class InboxFrameworks
{
    private SortedDictionary<string, SortedDictionary<Version, Version>> inboxVersions = new SortedDictionary<string, SortedDictionary<Version, Version>>();

    public int Count { get { return inboxVersions.Sum(m => m.Value.Count); } }

    public void AddInboxVersion(NuGetFramework framework, Version assemblyVersion)
    {
        var normalizedFramework = NormalizeFramework(framework);
        var frameworkKey = GetFrameworkKey(normalizedFramework);
        var frameworkVersion = normalizedFramework.Version;

        SortedDictionary<Version, Version> mappings;
        if (!inboxVersions.TryGetValue(frameworkKey, out mappings))
        {
            inboxVersions[frameworkKey] = mappings = new SortedDictionary<Version, Version>();
        }
        
        if (assemblyVersion == null)
        {
            // explicitly not supported.
            mappings[normalizedFramework.Version] = assemblyVersion;
        }
        else
        {
            List<Version> redundantMappings = new List<Version>();

            var addMapping = true;
            foreach(var mapping in mappings)
            {
                var existingFrameworkVersion = mapping.Key;
                var existingAssemblyVersion = mapping.Value;

                if (existingFrameworkVersion <= frameworkVersion)
                {
                    if (existingAssemblyVersion != null && existingAssemblyVersion >= assemblyVersion)
                    {
                        // lower or same framework already maps this or higher version, don't add it
                        addMapping = false;
                    }
                }
                else
                {
                    if (existingAssemblyVersion <= assemblyVersion)
                    {
                        // higher framework version with an equal or lower assembly version, remove it.
                        redundantMappings.Add(mapping.Key);
                    }
                }
            }

            if (addMapping)
            {
                mappings[normalizedFramework.Version] = assemblyVersion;
            }

            foreach(var redundantMapping in redundantMappings)
            {
                mappings.Remove(redundantMapping);
            }
        }


    }

    public IEnumerable<NuGetFramework> GetInboxFrameworks()
    {
        foreach (var framework in inboxVersions)
        {
            foreach (var frameworkVersion in framework.Value.Keys)
            {
                var fx = FromFrameworkKeyAndVersion(framework.Key, frameworkVersion);

                yield return fx;
            }
        }
    }

    public IEnumerable<KeyValuePair<NuGetFramework, Version>> GetInboxVersions()
    {
        foreach (var framework in inboxVersions)
        {
            foreach (var frameworkInboxVersionPair in framework.Value)
            {
                var frameworkVersion = frameworkInboxVersionPair.Key;
                var assemblyVersion = frameworkInboxVersionPair.Value;

                var fx = FromFrameworkKeyAndVersion(framework.Key, frameworkVersion);

                yield return new KeyValuePair<NuGetFramework, Version>(fx, assemblyVersion);
            }
        }
    }

    public bool IsAnyVersionInbox(NuGetFramework framework)
    {
        var normalizedFramework = NormalizeFramework(framework);
        var key = GetFrameworkKey(normalizedFramework);

        SortedDictionary<Version, Version> mappings;
        if (!inboxVersions.TryGetValue(key, out mappings))
        {
            // no inbox info for this framework
            return false;
        }

        return mappings.Keys.Any(fxVer => fxVer <= framework.Version);
    }

    public bool IsInbox(NuGetFramework framework, Version assemblyVersion, bool permitRevisions = false)
    {
        var normalizedFramework = NormalizeFramework(framework);
        var key = GetFrameworkKey(normalizedFramework);

        SortedDictionary<Version, Version> mappings;
        if (!inboxVersions.TryGetValue(key, out mappings))
        {
            // no inbox info for this framework
            return false;
        }

        Version assemblyVersionInbox;
        if (mappings.TryGetValue(normalizedFramework.Version, out assemblyVersionInbox))
        {
            if (assemblyVersionInbox == null)
            {
                // null entry means it's explicitly not inbox
                return false;
            }

            if (assemblyVersionInbox == VersionUtility.MaxVersion)
            {
                return true;
            }

            // inbox if explicit entry is greater than or equal to current
            return permitRevisions ? 
                VersionUtility.IsCompatibleApiVersion(assemblyVersionInbox, assemblyVersion) : 
                assemblyVersionInbox >= assemblyVersion;
        }

        // find nearest
        var compatibleMapping = mappings.LastOrDefault(m => m.Key < normalizedFramework.Version);
        if (compatibleMapping.Key == null || compatibleMapping.Value == null)
        {
            // either no compatible mapping, or compatible mapping explicitly not inbox
            return false;
        }

        if (compatibleMapping.Value == VersionUtility.MaxVersion)
        {
            return true;
        }

        // inbox if compatible entry is greater than or equal to current
        return permitRevisions ?
                VersionUtility.IsCompatibleApiVersion(compatibleMapping.Value, assemblyVersion) :
                compatibleMapping.Value >= assemblyVersion;
    }


    private static NuGetFramework NormalizeFramework(NuGetFramework framework)
    {
        if (framework == FrameworkConstants.CommonFrameworks.NetCore50)
        {
            // normalize netcore50 -> UAP10.
            // this permits us to model that netcore50/uap10 should not inherit inbox state from netcore4*
            return FrameworkConstants.CommonFrameworks.UAP10;
        }
        else if (framework == FrameworkConstants.CommonFrameworks.NetCore45)
        {
            return FrameworkConstants.CommonFrameworks.Win8;
        }
        else if (framework == FrameworkConstants.CommonFrameworks.NetCore451)
        {
            return FrameworkConstants.CommonFrameworks.Win81;
        }

        return framework;
    }

    private static string GetFrameworkKey(NuGetFramework framework)
    {
        if (!String.IsNullOrEmpty(framework.Profile))
        {
            return framework.Framework + "," + framework.Profile;
        }
        return framework.Framework;
    }

    private static NuGetFramework FromFrameworkKeyAndVersion(string key, Version version)
    {
        var parts = key.Split(',');

        if (parts.Length > 1)
        {
            return new NuGetFramework(parts[0], version, parts[1]);
        }
        else
        {
            return new NuGetFramework(key, version);
        }
    }
}

public class InboxFrameworksConverter : JsonConverter
{
    private const string AnyVersion = "Any";
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(InboxFrameworks);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jobj = JObject.Load(reader);

        var result = new InboxFrameworks();
        foreach (var property in jobj.Properties())
        {
            var versionString = property.Value.ToString();
            var version = versionString.Equals(AnyVersion, StringComparison.OrdinalIgnoreCase) ? VersionUtility.MaxVersion : new Version(versionString);
            result.AddInboxVersion(NuGetFramework.Parse(property.Name), version);
        }

        return result;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
        }
        else if (value is InboxFrameworks)
        {
            writer.WriteStartObject();

            foreach(var frameworkPair in ((InboxFrameworks)value).GetInboxVersions())
            {
                var shortName = frameworkPair.Key.GetShortFolderName();
                var assemblyVersion = frameworkPair.Value;
                var assemblyVersionString = assemblyVersion == VersionUtility.MaxVersion ? AnyVersion : assemblyVersion.ToString();
                writer.WritePropertyName(shortName);
                writer.WriteValue(assemblyVersionString);
            }

            writer.WriteEndObject();
        }
        else
        {
            throw new JsonSerializationException($"Expected {nameof(InboxFrameworks)} but got {value.GetType()}");
        }
    }
}

public class MetaPackages
{
    private Dictionary<string, string> packageToMetaPackage = new Dictionary<string, string>();

    public void AddMetaPackageMapping(string packageId, string metaPackageId)
    {
        string existingMetaPackage;

        if (packageToMetaPackage.TryGetValue(packageId, out existingMetaPackage))
        {
            if (existingMetaPackage != metaPackageId)
            {
                throw new InvalidOperationException($"Package {packageId} cannot be mapped to {metaPackageId} because it is already mapped to {existingMetaPackage}.");
            }
        }
        else
        {
            packageToMetaPackage.Add(packageId, metaPackageId);
        }
    }

    public string GetMetaPackageId(string packageId)
    {
        string metaPackageId;

        packageToMetaPackage.TryGetValue(packageId, out metaPackageId);

        return metaPackageId;
    }

    public void Merge(MetaPackages other)
    {
        foreach(var metaPackage in other.packageToMetaPackage)
        {
            // only merge a meta-package definition if it wasn't defined
            if (!packageToMetaPackage.ContainsKey(metaPackage.Key))
            {
                packageToMetaPackage.Add(metaPackage.Key, metaPackage.Value);
            }
        }
    }

    internal IEnumerable<IGrouping<string, string>> GetMetaPackageGrouping()
    {
        return packageToMetaPackage.GroupBy(p => p.Value, p => p.Key);
    }
}

public class MetaPackagesConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(MetaPackages);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jobj = JObject.Load(reader);

        var result = new MetaPackages();
        foreach (var property in jobj.Properties())
        {
            if (property.Value == null || property.Value.Type == JTokenType.Null)
            {
                continue;
            }

            var metaPackageId = property.Name;
            
            var metaPackageArray = property.Value as JArray;

            if (metaPackageArray == null)
            {
                throw new JsonSerializationException($"Expected array for property {metaPackageId}");
            }

            foreach(var package in metaPackageArray)
            {
                result.AddMetaPackageMapping(package.ToString(), metaPackageId);
            }
        }

        return result;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
        }
        else if (value is MetaPackages)
        {
            writer.WriteStartObject();
            foreach(var metaPackage in ((MetaPackages)value).GetMetaPackageGrouping())
            {
                writer.WritePropertyName(metaPackage.Key);
                writer.WriteStartArray();
                foreach(var package in metaPackage)
                {
                    writer.WriteValue(package);
                }
                writer.WriteEndArray();
            }

            writer.WriteEndObject();
        }
        else
        {
            throw new JsonSerializationException($"Expected {nameof(MetaPackages)} but got {value.GetType()}");
        }
    }
}