File: FilterStaticWebAssetEndpoints.cs
Web Access
Project: ..\..\..\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using Microsoft.Build.Framework;
 
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
// Filters the list of endpoints based on the provided criteria
// The endpoints must meet all the criteria to be included.
// The criteria are:
// - Assets: The asset file that the endpoint is associated with must be in the list of assets.
// - Properties: The endpoint must have all the properties specified in the list of properties with
//   optionally a specific value.
// - Selectors: The endpoint must have all the selectors specified in the list of selectors with
//   optionally a specific value.
// - Headers: The endpoint must have all the headers specified in the list of headers with
//   optionally a specific value.
public class FilterStaticWebAssetEndpoints : Task
{
    public ITaskItem[] Endpoints { get; set; }
 
    public ITaskItem[] Assets { get; set; }
 
    public ITaskItem[] Filters { get; set; }
 
    [Output] public ITaskItem[] FilteredEndpoints { get; set; }
 
    [Output] public ITaskItem[] AssetsWithoutMatchingEndpoints { get; set; }
 
    public override bool Execute()
    {
        var filterCriteria = (Filters ?? []).Select(FilterCriteria.FromTaskItem).ToArray();
        var assetFiles = Assets != null ? StaticWebAsset.ToAssetDictionary(Assets) : [];
        var endpoints = StaticWebAssetEndpoint.FromItemGroup(Endpoints ?? []);
        var endpointFoundMatchingAsset = new Dictionary<string, StaticWebAsset>();
 
        var filteredEndpoints = new List<StaticWebAssetEndpoint>();
        for (var i = 0; i < endpoints.Length; i++)
        {
            StaticWebAsset asset = null;
            var endpoint = endpoints[i];
            if (assetFiles.Count > 0 && !assetFiles.TryGetValue(endpoint.AssetFile, out asset))
            {
                continue;
            }
 
            if (MeetsAllCriteria(endpoint, asset, filterCriteria, out var failingCriteria))
            {
                if (asset != null && !endpointFoundMatchingAsset.ContainsKey(asset.Identity))
                {
                    endpointFoundMatchingAsset.Add(asset.Identity, asset);
                }
                filteredEndpoints.Add(endpoint);
            }
            else
            {
                Log.LogMessage(
                    MessageImportance.Low,
                    $"Filtered out endpoint {endpoint.Route} because it did not meet the criteria {failingCriteria.Type} {failingCriteria.Name} {failingCriteria.Value}");
            }
        }
 
        FilteredEndpoints = filteredEndpoints.Select(e => e.ToTaskItem()).ToArray();
 
        foreach (var asset in endpointFoundMatchingAsset)
        {
            assetFiles.Remove(asset.Key);
        }
 
        AssetsWithoutMatchingEndpoints = assetFiles.Values.Select(a => a.ToTaskItem()).ToArray();
        return !Log.HasLoggedErrors;
    }
 
    private static bool MeetsAllCriteria(StaticWebAssetEndpoint endpoint, StaticWebAsset asset, FilterCriteria[] filterCriteria, out FilterCriteria failingCriteria)
    {
        for (var i = 0; i < filterCriteria.Length; i++)
        {
            var criteria = filterCriteria[i];
            switch (criteria.Type)
            {
                case "Property":
                    var meetsPropertyCriteria = criteria.ExcludeOnMatch();
                    for (var j = 0; j < endpoint.EndpointProperties.Length; j++)
                    {
                        var property = endpoint.EndpointProperties[j];
                        if (MeetsCriteria(criteria, property.Name, property.Value))
                        {
                            meetsPropertyCriteria = !criteria.ExcludeOnMatch();
                            break;
                        }
                    }
                    if (!meetsPropertyCriteria)
                    {
                        failingCriteria = criteria;
                        return false;
                    }
                    break;
                case "Selector":
                    var meetsSelectorCriteria = criteria.ExcludeOnMatch();
                    for (var j = 0; j < endpoint.Selectors.Length; j++)
                    {
                        var selector = endpoint.Selectors[j];
                        if (MeetsCriteria(criteria, selector.Name, selector.Value))
                        {
                            meetsSelectorCriteria = !criteria.ExcludeOnMatch();
                            break;
                        }
                    }
                    if (!meetsSelectorCriteria)
                    {
                        failingCriteria = criteria;
                        return false;
                    }
                    break;
                case "Header":
                    var meetsHeaderCriteria = criteria.ExcludeOnMatch();
                    for (var j = 0; j < endpoint.ResponseHeaders.Length; j++)
                    {
                        var header = endpoint.ResponseHeaders[j];
                        if (MeetsCriteria(criteria, header.Name, header.Value))
                        {
                            meetsHeaderCriteria = !criteria.ExcludeOnMatch();
                            break;
                        }
                    }
                    if (!meetsHeaderCriteria)
                    {
                        failingCriteria = criteria;
                        return false;
                    }
                    break;
                case "Standalone":
                    if (asset == null)
                    {
                        failingCriteria = criteria;
                        return false;
                    }
                    var path = asset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance);
                    var route = asset.ReplaceTokens(endpoint.Route, StaticWebAssetTokenResolver.Instance);
                    if (!string.Equals(route, path, StringComparison.OrdinalIgnoreCase) || criteria.ExcludeOnMatch())
                    {
                        failingCriteria = criteria;
                        return false;
                    }
                    break;
                default:
                    throw new InvalidOperationException($"Unknown criteria type {criteria.Type}");
            }
        }
 
        failingCriteria = null;
        return true;
    }
 
    private static bool MeetsCriteria(
        FilterCriteria criteria,
        string name, string value) =>
            string.Equals(name, criteria.Name, StringComparison.OrdinalIgnoreCase) &&
            (string.IsNullOrEmpty(criteria.Value) || string.Equals(value, criteria.Value, StringComparison.Ordinal));
 
    private sealed class FilterCriteria(string type, string name, string value, string mode)
    {
        public string Type { get; } = type;
        public string Name { get; } = name;
        public string Value { get; } = value;
        public string Mode { get; } = mode ?? "Include";
 
        public bool ExcludeOnMatch() => string.Equals(Mode, "Exclude", StringComparison.OrdinalIgnoreCase);
 
        public static FilterCriteria FromTaskItem(ITaskItem item) => new(
                item.ItemSpec,
                item.GetMetadata("Name"),
                item.GetMetadata("Value"),
                item.GetMetadata("Mode"));
    }
}