Switching search to generate a single query per request, and adding unit tests (#73)

This commit is contained in:
Union Palenshus 2021-10-26 19:26:50 -07:00 коммит произвёл GitHub
Родитель b50c734f40
Коммит 3043f8c1aa
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
39 изменённых файлов: 1313 добавлений и 679 удалений

1
.gitattributes поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
manifests.json filter=lfs diff=lfs merge=lfs -text

5
.gitignore поставляемый
Просмотреть файл

@ -352,9 +352,12 @@ MigrationBackup/
# Ignore JetBrains Files
src/.idea/
# Exlude Function Settings
# Exclude Function Settings
src/WinGet.RestSource.Functions/local.settings.json
# Exclude local test settings
test.runsettings.json
# Exclude Documentation XML
src/*/Microsoft.WinGet.RestSource*.Documentation.xml

Просмотреть файл

@ -1,5 +1,18 @@
# Welcome to the winget-cli-restsource repository
## Building the client
### Prerequisites
* [Git Large File Storage (LFS)](https://git-lfs.github.com/)
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/)
* The following workloads:
* .NET desktop development
* Azure development
* ASP<area>.NET and web development
Open `src\WinGet.RestSource.sln` in Visual Studio and build. We currently only build using the solution; command line methods of building a VS solution should work as well.
## Running locally
The REST functions can be run locally, but to use winget with them, the functions must be run using HTTPS, this is pre-configured by the `launchSettings.json` file.
@ -16,6 +29,19 @@ The REST functions can be run locally, but to use winget with them, the function
Your commands to winget will now use your locally running REST instance as the primary source.
## Running Unit Tests
Running unit tests are a great way to ensure that functionality is preserved across major changes. You can run these tests in Visual Studio Test Explorer.
### Testing Prerequisites
* Install the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21)
* Copy the `WinGet.RestSource.UnitTest\Test.runsettings.template.json` template configuration to `Test.runsettings.json`
* The defaults should work for your local Cosmos DB emulator instance. You can change the configuration to point to a Cosmos DB instance in Azure instead.
* Alternatively, all of the test configuration properties can be set as environment variables. This is useful for overriding properties in an ADO build.
In Visual Studio, run the tests from the menu with Test > Run All Tests
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a

Просмотреть файл

@ -1,6 +1,11 @@
# Template helper to restore, build, and publish
steps:
# Checkout repo with lfs enabled
- checkout: self
lfs: "true"
## Restore
- task: DotNetCoreCLI@2
displayName: 'Restore'

Просмотреть файл

@ -7,6 +7,15 @@ parameters:
source: ''
steps:
- powershell: |
# Copy and rename Test.runsettings.template.json to Test.runsettings.json to be used as test config
copy "$(Build.SourcesDirectory)\src\WinGet.RestSource.UnitTest\Test.runsettings.template.json" "${{ parameters.source }}\Test.runsettings.json"
# Launch Cosmos DB emulator
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator
displayName: "Setup test pre-requisites"
- task: CopyFiles@2
displayName: 'Copy Files: ${{ parameters.name }}'
inputs:
@ -25,6 +34,7 @@ steps:
codeCoverageEnabled: true
platform: '$(BuildPlatform)'
configuration: '$(BuildConfiguration)'
overrideTestrunParameters: '-CosmosAccountEndpoint "$(CosmosDbEmulator.Endpoint)"'
condition: succeeded()
- task: PublishBuildArtifacts@1

Просмотреть файл

@ -1,33 +1,33 @@
# Copyright (c) Microsoft Corporation. All rights reserved
# CI pipeline for winget-cli-restsource
# Branches that trigger a build on commit
trigger:
- main
# PR triggers
pr:
branches:
include:
- main
paths:
include:
- pipelines/*
- src/*
jobs:
- job: 'BuildTestPublish'
displayName: 'Build, Publish & Test'
timeoutInMinutes: 60
pool:
vmImage: windows-2019
demands:
- msbuild
- visualstudio
variables:
BuildConfiguration: 'release'
BuildPlatform: 'Any CPU'
steps:
# Restore and Build
# Copyright (c) Microsoft Corporation. All rights reserved
# CI pipeline for winget-cli-restsource
# Branches that trigger a build on commit
trigger:
- main
# PR triggers
pr:
branches:
include:
- main
paths:
include:
- pipelines/*
- src/*
jobs:
- job: 'BuildTestPublish'
displayName: 'Build, Publish & Test'
timeoutInMinutes: 60
pool:
vmImage: windows-2019
demands:
- msbuild
- visualstudio
variables:
BuildConfiguration: 'release'
BuildPlatform: 'Any CPU'
steps:
# Restore and Build
- template: templates/restore-build-publish-test.yml

Просмотреть файл

@ -213,7 +213,7 @@ namespace Microsoft.WinGet.RestSource.Functions
// Parse Headers
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
installers = await this.dataStore.GetInstallers(packageIdentifier, packageVersion, installerIdentifier, null);
installers = await this.dataStore.GetInstallers(packageIdentifier, packageVersion, installerIdentifier);
}
catch (DefaultException e)
{

Просмотреть файл

@ -210,7 +210,7 @@ namespace Microsoft.WinGet.RestSource.Functions
// Parse Headers
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
locales = await this.dataStore.GetLocales(packageIdentifier, packageVersion, packageLocale, null);
locales = await this.dataStore.GetLocales(packageIdentifier, packageVersion, packageLocale);
}
catch (DefaultException e)
{

Просмотреть файл

@ -60,12 +60,13 @@ namespace Microsoft.WinGet.RestSource.Functions
{
// Parse Headers
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
string continuationToken = headers.GetValueOrDefault(HeaderConstants.ContinuationToken);
// Get Manifest Search Request and Validate.
ManifestSearchRequest manifestSearch = await Parser.StreamParser<ManifestSearchRequest>(req.Body, log);
ApiDataValidator.Validate(manifestSearch);
manifestSearchResponse = await this.dataStore.SearchPackageManifests(manifestSearch, headers, req.Query);
manifestSearchResponse = await this.dataStore.SearchPackageManifests(manifestSearch, continuationToken);
unsupportedFields = UnsupportedAndRequiredFieldsHelper.GetUnsupportedPackageMatchFieldsFromSearchRequest(manifestSearch, ApiConstants.UnsupportedPackageMatchFields);
requiredFields = UnsupportedAndRequiredFieldsHelper.GetRequiredPackageMatchFieldsFromSearchRequest(manifestSearch, ApiConstants.RequiredPackageMatchFields);

Просмотреть файл

@ -191,7 +191,7 @@ namespace Microsoft.WinGet.RestSource.Functions
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
// Fetch Results
packages = await this.dataStore.GetPackages(packageIdentifier, req.Query);
packages = await this.dataStore.GetPackages(packageIdentifier, req.Query[QueryConstants.ContinuationToken]);
}
catch (DefaultException e)
{

Просмотреть файл

@ -189,8 +189,21 @@ namespace Microsoft.WinGet.RestSource.Functions
// Parse Headers
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
string continuationToken = null;
string versionFilter = null;
string channelFilter = null;
string marketFilter = null;
// Schema supports query parameters only when PackageIdentifier is specified.
manifests = await this.dataStore.GetPackageManifests(packageIdentifier, string.IsNullOrWhiteSpace(packageIdentifier) ? null : req.Query);
if (!string.IsNullOrWhiteSpace(packageIdentifier))
{
continuationToken = req.Query[QueryConstants.ContinuationToken];
versionFilter = req.Query[QueryConstants.Version];
channelFilter = req.Query[QueryConstants.Channel];
marketFilter = req.Query[QueryConstants.Market];
}
manifests = await this.dataStore.GetPackageManifests(packageIdentifier, continuationToken, versionFilter, channelFilter, marketFilter);
unsupportedQueryParameters = UnsupportedAndRequiredFieldsHelper.GetUnsupportedQueryParametersFromRequest(req.Query, ApiConstants.UnsupportedQueryParameters);
requiredQueryParameters = UnsupportedAndRequiredFieldsHelper.GetRequiredQueryParametersFromRequest(req.Query, ApiConstants.RequiredQueryParameters);
}

Просмотреть файл

@ -2,65 +2,65 @@
// <copyright file="ServerFunctions.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Functions
{
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Functions
{
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.WinGet.RestSource.Cosmos;
using Microsoft.WinGet.RestSource.Functions.Common;
using Microsoft.WinGet.RestSource.Functions.Common;
using Microsoft.WinGet.RestSource.Utils.Constants;
using Microsoft.WinGet.RestSource.Utils.Exceptions;
using Microsoft.WinGet.RestSource.Utils.Models;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
/// <summary>
/// This class contains the functions for interacting with packages.
/// </summary>
public class ServerFunctions
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerFunctions"/> class.
/// </summary>
public ServerFunctions()
{
}
/// <summary>
/// Server Information Get Function.
/// This allows us to make Get Server Information.
/// </summary>
/// <param name="req">HttpRequest.</param>
/// <param name="log">ILogger.</param>
/// <returns>IActionResult.</returns>
[FunctionName(FunctionConstants.InformationGet)]
public IActionResult InformationGetAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, FunctionConstants.FunctionGet, Route = "information")]
HttpRequest req,
ILogger log)
{
/// <summary>
/// This class contains the functions for interacting with packages.
/// </summary>
public class ServerFunctions
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerFunctions"/> class.
/// </summary>
public ServerFunctions()
{
}
/// <summary>
/// Server Information Get Function.
/// This allows us to make Get Server Information.
/// </summary>
/// <param name="req">HttpRequest.</param>
/// <param name="log">ILogger.</param>
/// <returns>IActionResult.</returns>
[FunctionName(FunctionConstants.InformationGet)]
public IActionResult InformationGetAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, FunctionConstants.FunctionGet, Route = "information")]
HttpRequest req,
ILogger log)
{
Information information;
try
{
information = new Information();
}
catch (DefaultException e)
{
log.LogError(e.ToString());
return ActionResultHelper.ProcessError(e.InternalRestError);
}
catch (Exception e)
{
log.LogError(e.ToString());
return ActionResultHelper.UnhandledError(e);
}
return new ApiObjectResult(new ApiResponse<Information>(information));
}
}
}
try
{
information = new Information();
}
catch (DefaultException e)
{
log.LogError(e.ToString());
return ActionResultHelper.ProcessError(e.InternalRestError);
}
catch (Exception e)
{
log.LogError(e.ToString());
return ActionResultHelper.UnhandledError(e);
}
return new ApiObjectResult(new ApiResponse<Information>(information));
}
}
}

Просмотреть файл

@ -196,7 +196,7 @@ namespace Microsoft.WinGet.RestSource.Functions
// Parse Headers
Dictionary<string, string> headers = HeaderProcessor.ToDictionary(req.Headers);
versions = await this.dataStore.GetVersions(packageIdentifier, packageVersion, null);
versions = await this.dataStore.GetVersions(packageIdentifier, packageVersion);
}
catch (DefaultException e)
{

Просмотреть файл

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>V3</AzureFunctionsVersion>
<Platforms>AnyCPU</Platforms>
<AssemblyName>Microsoft.WinGet.RestSource.Functions</AssemblyName>
<RootNamespace>Microsoft.WinGet.RestSource.Functions</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>V3</AzureFunctionsVersion>
<Platforms>AnyCPU</Platforms>
<AssemblyName>Microsoft.WinGet.RestSource.Functions</AssemblyName>
<RootNamespace>Microsoft.WinGet.RestSource.Functions</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<DebugType>full</DebugType>
@ -13,48 +13,49 @@
<NoWarn>1701;1702;NU1701</NoWarn>
<DocumentationFile>$(SolutionDir)WinGet.RestSource.Functions\Microsoft.WinGet.RestSource.Functions.Documentation.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<WarningsAsErrors />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="4.0.0-preview2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<WarningsAsErrors />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.7" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
</ItemGroup>
<ItemGroup>
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version, most likely when updating to dotnet 5.0 -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.template.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.7" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinGet.RestSource\WinGet.RestSource.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.template.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinGet.RestSource\WinGet.RestSource.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -8,6 +8,6 @@
"CosmosReadWriteKey": "<your CosmosDB read-write account key>",
"CosmosDatabase": "<your CosmosDB database>",
"CosmosContainer": "<your CosmosDB container>",
"ServerIdentifier": "winget-pkgs-restsource-dev"
"ServerIdentifier": "winget-cli-restsource-dev"
}
}

Просмотреть файл

@ -28,7 +28,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version-->
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version, most likely when updating to dotnet 5.0 -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="YamlDotNet" Version="8.1.2" />
<PackageReference Include="Microsoft.WindowsPackageManager.Utils" Version="0.3.4" GeneratePathProperty="true" />

Просмотреть файл

@ -0,0 +1,52 @@
// -----------------------------------------------------------------------
// <copyright file="Util.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Winget.RestSource.UnitTest.Common
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Test helper utilities.
/// </summary>
public static class Util
{
/// <summary>
/// Executes a foreach async operation on an enumerable source in which iterations are run in parallel.
/// </summary>
/// <typeparam name="TSource">The type of the elements in source.</typeparam>
/// <param name="source">The enumerable that contains the original data source.</param>
/// <param name="body">The async delegate that is invoked once per iteration.</param>
/// <param name="maxDegreeOfParallelism">The maximum number of concurrent tasks.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
public static async Task AsyncParallelForEach<TSource>(this IEnumerable<TSource> source, Func<TSource, Task> body, int maxDegreeOfParallelism)
{
var semaphore = new Semaphore(maxDegreeOfParallelism, maxDegreeOfParallelism);
var tasks = new List<Task>();
foreach (var item in source)
{
semaphore.WaitOne();
tasks.Add(Task.Run(async () =>
{
try
{
await body(item);
}
finally
{
semaphore.Release();
}
}));
}
// Wait until all are done
await Task.WhenAll(tasks);
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"CosmosAccountEndpoint": "https://localhost:8081/",
// These are well-known hardcoded keys used by the emulator, not secrets https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#authenticate-requests
"CosmosReadOnlyKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"CosmosReadWriteKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"CosmosDatabase": "cosmossql-pkgsrest-test",
"CosmosContainer": "ManifestsTest",
"ServerIdentifier": "winget-cli-restsource-test"
}

Просмотреть файл

@ -0,0 +1,279 @@
// -----------------------------------------------------------------------
// <copyright file="CosmosDataStoreTests.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Winget.RestSource.UnitTest.Tests.RestSource.Cosmos
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.WinGet.RestSource.Cosmos;
using Microsoft.Winget.RestSource.UnitTest.Common;
using Microsoft.WinGet.RestSource.Utils.Constants;
using Microsoft.WinGet.RestSource.Utils.Constants.Enumerations;
using Microsoft.WinGet.RestSource.Utils.Models.ExtendedSchemas;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
using Arrays = Microsoft.WinGet.RestSource.Utils.Models.Arrays;
using Objects = Microsoft.WinGet.RestSource.Utils.Models.Objects;
/// <summary>
/// CosmosDataStore Tests.
/// </summary>
public class CosmosDataStoreTests : IAsyncLifetime
{
private const string ManifestsPath = @"Tests\RestSource\Cosmos\manifests.json";
private const int ManifestCount = 500;
private const int TestDatabaseRUs = 4000;
private const string PowerToysPackageIdentifier = "Microsoft.PowerToys";
private readonly ITestOutputHelper log;
private readonly IConfigurationRoot configuration;
private readonly CosmosDataStore cosmosDataStore;
private IList<CosmosPackageManifest> allTestManifests;
/// <summary>
/// Initializes a new instance of the <see cref="CosmosDataStoreTests"/> class.
/// </summary>
/// <param name="log">ITestOutputHelper.</param>
public CosmosDataStoreTests(ITestOutputHelper log)
{
this.log = log;
this.configuration = new ConfigurationBuilder()
// Defaults specified in the Test.runsettings.json
.AddJsonFile("Test.runsettings.json", true)
// But they can be overridden using environment variables
.AddEnvironmentVariables()
.Build();
string endpoint = this.configuration[CosmosConnectionConstants.CosmosAccountEndpointSetting] ?? throw new ArgumentNullException();
string readOnlyKey = this.configuration[CosmosConnectionConstants.CosmosReadOnlyKeySetting] ?? throw new ArgumentNullException();
string readWriteKey = this.configuration[CosmosConnectionConstants.CosmosReadWriteKeySetting] ?? throw new ArgumentNullException();
string databaseId = this.configuration[CosmosConnectionConstants.DatabaseNameSetting] ?? throw new ArgumentNullException();
string containerId = this.configuration[CosmosConnectionConstants.ContainerNameSetting] ?? throw new ArgumentNullException();
this.log.WriteLine($"{CosmosConnectionConstants.CosmosAccountEndpointSetting}: {endpoint}");
this.log.WriteLine($"{CosmosConnectionConstants.CosmosReadOnlyKeySetting}: {readOnlyKey}");
this.log.WriteLine($"{CosmosConnectionConstants.CosmosReadWriteKeySetting}: {readWriteKey}");
this.log.WriteLine($"{CosmosConnectionConstants.DatabaseNameSetting}: {databaseId}");
this.log.WriteLine($"{CosmosConnectionConstants.ContainerNameSetting}: {containerId}");
var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<CosmosDataStore>();
this.cosmosDataStore = new CosmosDataStore(logger, endpoint, readWriteKey, readOnlyKey, databaseId, containerId);
}
/// <inheritdoc/>
public async Task InitializeAsync()
{
var sw = Stopwatch.StartNew();
string json = System.IO.File.ReadAllText(ManifestsPath);
this.allTestManifests = JsonConvert.DeserializeObject<List<CosmosPackageManifest>>(json);
// Ensure container exists prior to getting count
await this.cosmosDataStore.CreateContainer(TestDatabaseRUs);
int itemCount = await this.cosmosDataStore.Count();
if (itemCount == ManifestCount)
{
this.log.WriteLine($"Test container already contains the expected number of items ({ManifestCount}), no need to re-initialize");
}
else
{
this.log.WriteLine($"Test container does not contain the expected number of items, re-initializing. Expected: {ManifestCount}, Actual: {itemCount}");
await this.cosmosDataStore.DeleteContainer();
await this.cosmosDataStore.CreateContainer(TestDatabaseRUs);
// Only add the first 500 manifests to the database.
await this.allTestManifests.Take(ManifestCount).AsyncParallelForEach(manifest => this.cosmosDataStore.AddPackageManifest(manifest), 30);
}
this.log.WriteLine($"Initialization completed in {sw.Elapsed}");
}
/// <inheritdoc/>
public Task DisposeAsync()
{
return Task.CompletedTask;
}
/// <summary>
/// Verifies the various CRUD operations exposed by the CosmosDataStore.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task CreateUpdateReadDeleteTest()
{
var addedManifest = this.allTestManifests.Skip(ManifestCount).First();
var packageManifestsResult = await this.cosmosDataStore.GetPackageManifests(addedManifest.PackageIdentifier);
Assert.Empty(packageManifestsResult.Items);
await this.cosmosDataStore.AddPackageManifest(addedManifest);
Assert.Equal(ManifestCount + 1, await this.cosmosDataStore.Count());
packageManifestsResult = await this.cosmosDataStore.GetPackageManifests(addedManifest.PackageIdentifier);
Assert.Equal(addedManifest.PackageIdentifier, packageManifestsResult.Items.Single().PackageIdentifier);
string updatedName = "BOGUS_NAME";
addedManifest.Versions.First().DefaultLocale.PackageName = updatedName;
await this.cosmosDataStore.UpdatePackageManifest(addedManifest.PackageIdentifier, addedManifest);
packageManifestsResult = await this.cosmosDataStore.GetPackageManifests(addedManifest.PackageIdentifier);
Assert.Equal(updatedName, packageManifestsResult.Items.Single().Versions.First().DefaultLocale.PackageName);
await this.cosmosDataStore.DeletePackageManifest(addedManifest.PackageIdentifier);
Assert.Equal(ManifestCount, await this.cosmosDataStore.Count());
await this.cosmosDataStore.AddPackage(addedManifest);
Assert.Equal(ManifestCount + 1, await this.cosmosDataStore.Count());
var packageResult = await this.cosmosDataStore.GetPackages(addedManifest.PackageIdentifier);
Assert.Equal(addedManifest.PackageIdentifier, packageResult.Items.Single().PackageIdentifier);
var addedVersion = addedManifest.Versions.First();
await this.cosmosDataStore.AddVersion(addedManifest.PackageIdentifier, addedVersion);
await this.cosmosDataStore.AddInstaller(addedManifest.PackageIdentifier, addedVersion.PackageVersion, addedVersion.Installers.First());
await this.cosmosDataStore.AddLocale(addedManifest.PackageIdentifier, addedVersion.PackageVersion, addedVersion.DefaultLocale);
packageManifestsResult = await this.cosmosDataStore.GetPackageManifests(addedManifest.PackageIdentifier);
var resultVersion = packageManifestsResult.Items.First().Versions.First();
Assert.Equal(addedVersion.PackageVersion, resultVersion.PackageVersion);
Assert.Equal(addedVersion.DefaultLocale.PackageName, resultVersion.DefaultLocale.PackageName);
Assert.Equal(addedVersion.Installers.First().InstallerUrl, resultVersion.Installers.First().InstallerUrl);
await this.cosmosDataStore.DeletePackage(addedManifest.PackageIdentifier);
Assert.Equal(ManifestCount, await this.cosmosDataStore.Count());
}
/// <summary>
/// Verifies the GetPackage* APIs.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task GetPackages()
{
this.log.WriteLine("Tests that GetPackages returns the expected package.");
{
var packages = await this.cosmosDataStore.GetPackages(PowerToysPackageIdentifier, null);
Assert.NotEqual(0, packages.Items.Count);
Assert.Equal(PowerToysPackageIdentifier, packages.Items.First().PackageIdentifier);
}
this.log.WriteLine("Tests that ContinuationToken has an effect for GetPackages.");
{
var firstPackageSet = await this.cosmosDataStore.GetPackages(null);
Assert.Equal(FunctionSettingsConstants.MaxResultsPerPage, firstPackageSet.Items.Count);
var continuedPackageSet = await this.cosmosDataStore.GetPackages(null, firstPackageSet.ContinuationToken);
Assert.Equal(FunctionSettingsConstants.MaxResultsPerPage, continuedPackageSet.Items.Count);
Assert.False(firstPackageSet.Items.Intersect(continuedPackageSet.Items).Any());
}
this.log.WriteLine("Tests that GetPackageManifests returns the expected package.");
{
var packageManifests = await this.cosmosDataStore.GetPackageManifests(PowerToysPackageIdentifier);
Assert.NotEqual(0, packageManifests.Items.Count);
Assert.Equal(PowerToysPackageIdentifier, packageManifests.Items.First().PackageIdentifier);
}
this.log.WriteLine("Tests that GetPackageManifests returns the expected package and version.");
{
const string version = "0.37.0";
var packageManifests = await this.cosmosDataStore.GetPackageManifests(PowerToysPackageIdentifier, null, version);
Assert.NotEqual(0, packageManifests.Items.Count);
Assert.Equal(PowerToysPackageIdentifier, packageManifests.Items.First().PackageIdentifier);
Assert.Equal(version, packageManifests.Items.First().Versions.Single().PackageVersion);
}
this.log.WriteLine("Tests that ContinuationToken has an effect for GetPackageManifests.");
{
var firstPackageManifestSet = await this.cosmosDataStore.GetPackageManifests(null);
Assert.Equal(FunctionSettingsConstants.MaxResultsPerPage, firstPackageManifestSet.Items.Count);
var continuedPackageManifestSet = await this.cosmosDataStore.GetPackageManifests(null, firstPackageManifestSet.ContinuationToken);
Assert.Equal(FunctionSettingsConstants.MaxResultsPerPage, continuedPackageManifestSet.Items.Count);
Assert.False(firstPackageManifestSet.Items.Intersect(continuedPackageManifestSet.Items).Any());
}
}
/// <summary>
/// Verifies that the CosmosDataStore correctly handles a search using a search query term.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task SearchUsingQuery()
{
this.log.WriteLine("Tests that SearchPackageManifests returns the expected results when using the Query property.");
{
await this.TestSearchQuery("PowerToys", MatchType.Exact, PowerToysPackageIdentifier);
await this.TestSearchQuery("powertoys", MatchType.CaseInsensitive, PowerToysPackageIdentifier);
await this.TestSearchQuery("PowerT", MatchType.StartsWith, PowerToysPackageIdentifier);
await this.TestSearchQuery("owerToy", MatchType.Substring, PowerToysPackageIdentifier);
}
this.log.WriteLine("Tests that using ContinuationToken with SearchPackageManifests allows us to retrieve all manifests.");
{
var allResults = new HashSet<ManifestSearchResponse>();
string continuationToken = null;
do
{
var manifestSearchRequest = new ManifestSearchRequest();
var result = await this.cosmosDataStore.SearchPackageManifests(manifestSearchRequest, continuationToken);
allResults.UnionWith(result.Items);
continuationToken = result.ContinuationToken;
}
while (continuationToken != null);
Assert.Equal(ManifestCount, allResults.Count);
}
}
/// <summary>
/// Verifies that the CosmosDataStore correctly handles a search using filters.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task SearchUsingFilter()
{
await this.TestSearchFilter(PackageMatchFields.PackageName, "PowerToys", MatchType.Exact, PowerToysPackageIdentifier);
await this.TestSearchFilter(PackageMatchFields.PackageName, "powertoys", MatchType.CaseInsensitive, PowerToysPackageIdentifier);
await this.TestSearchFilter(PackageMatchFields.PackageName, "PowerT", MatchType.StartsWith, PowerToysPackageIdentifier);
await this.TestSearchFilter(PackageMatchFields.PackageName, "owerToy", MatchType.Substring, PowerToysPackageIdentifier);
}
private async Task TestSearchQuery(string value, string matchType, string expectedPackageIdentifier)
{
var manifestSearchRequest = new ManifestSearchRequest();
manifestSearchRequest.Query = new Objects.SearchRequestMatch();
manifestSearchRequest.Query.KeyWord = value;
manifestSearchRequest.Query.MatchType = matchType;
var results = await this.cosmosDataStore.SearchPackageManifests(manifestSearchRequest, null);
Assert.NotEqual(0, results.Items.Count);
Assert.Equal(expectedPackageIdentifier, results.Items.First().PackageIdentifier);
}
private async Task TestSearchFilter(string packageMatchField, string value, string matchType, string expectedPackageIdentifier)
{
var manifestSearchRequest = new ManifestSearchRequest();
manifestSearchRequest.Filters = new Arrays.SearchRequestPackageMatchFilter
{
new Objects.SearchRequestPackageMatchFilter { PackageMatchField = packageMatchField, RequestMatch = new Objects.SearchRequestMatch { KeyWord = value, MatchType = matchType } },
};
var results = await this.cosmosDataStore.SearchPackageManifests(manifestSearchRequest, null);
Assert.Equal(1, results.Items.Count);
Assert.Equal(expectedPackageIdentifier, results.Items.Single().PackageIdentifier);
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58622012b34c49b71db97299f275175d855501412b5953762c7e6bf7c35fde8b
size 5439221

Просмотреть файл

@ -1,46 +1,63 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.Winget.RestSource.UnitTest</AssemblyName>
<RootNamespace>Microsoft.Winget.RestSource.UnitTest</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.Winget.RestSource.UnitTest</AssemblyName>
<RootNamespace>Microsoft.Winget.RestSource.UnitTest</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>$(SolutionDir)WinGet.RestSource.UnitTest\Microsoft.Winget.RestSource.UnitTest.Documentation.xml</DocumentationFile>
<NoWarn>1701;1702;NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup>
<DebugSymbols>true</DebugSymbols>
<NoWarn>1701;1702;NU1701</NoWarn>
<DocumentationFile>$(SolutionDir)WinGet.RestSource.UnitTest\Microsoft.Winget.RestSource.UnitTest.Documentation.xml</DocumentationFile>
<UserSecretsId>09404207-b3a3-4757-83f3-0a1ec9c39c4a</UserSecretsId>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>$(SolutionDir)WinGet.RestSource.UnitTest\Microsoft.Winget.RestSource.UnitTest.Documentation.xml</DocumentationFile>
<NoWarn>1701;1702;NU1701</NoWarn>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<WarningsAsErrors />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinGet.RestSource.Functions\WinGet.RestSource.Functions.csproj" />
<ProjectReference Include="..\WinGet.RestSource\WinGet.RestSource.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version, most likely when updating to dotnet 5.0 -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinGet.RestSource.Functions\WinGet.RestSource.Functions.csproj" />
<ProjectReference Include="..\WinGet.RestSource\WinGet.RestSource.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Test.runsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="Test.runsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tests\RestSource\Cosmos\manifests.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -34,5 +34,10 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// Gets or sets continuation Token.
/// </summary>
public string ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the request charge for this request from the Azure Cosmos DB service.
/// </summary>
public double RequestCharge { get; set; }
}
}

Просмотреть файл

@ -6,9 +6,7 @@
namespace Microsoft.WinGet.RestSource.Utils.Common
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
/// <summary>
@ -16,6 +14,12 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// </summary>
public interface IApiDataStore
{
/// <summary>
/// Returns the count of items in the data store.
/// </summary>
/// <returns>The number if items in the data store.</returns>
Task<int> Count();
/// <summary>
/// This will add a package to the data store.
/// </summary>
@ -42,9 +46,9 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// This will retrieve a set of packages based on a package identifier and a continuation token.
/// </summary>
/// <param name="packageIdentifier">Package Identifier.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <param name="continuationToken">(Optional) Continuation token.</param>
/// <returns>CosmosPage of Package Manifests.</returns>
Task<ApiDataPage<Package>> GetPackages(string packageIdentifier, IQueryCollection queryParameters);
Task<ApiDataPage<Package>> GetPackages(string packageIdentifier, string continuationToken = null);
/// <summary>
/// This will add a version to a package based on the package identifier.
@ -76,9 +80,8 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// </summary>
/// <param name="packageIdentifier">Package Identifier.</param>
/// <param name="packageVersion">Package Version.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <returns>CosmosPage of Version.</returns>
Task<ApiDataPage<Version>> GetVersions(string packageIdentifier, string packageVersion, IQueryCollection queryParameters);
Task<ApiDataPage<Version>> GetVersions(string packageIdentifier, string packageVersion);
/// <summary>
/// This will add an installer referencing a package identifier and a package version.
@ -114,9 +117,8 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// <param name="packageIdentifier">Package Identifier.</param>
/// <param name="packageVersion">Package Version.</param>
/// <param name="installerIdentifier">Installer Identifier.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <returns>CosmosPage of Installer.</returns>
Task<ApiDataPage<Installer>> GetInstallers(string packageIdentifier, string packageVersion, string installerIdentifier, IQueryCollection queryParameters);
Task<ApiDataPage<Installer>> GetInstallers(string packageIdentifier, string packageVersion, string installerIdentifier);
/// <summary>
/// This will add a locale referencing a package identifier and a package version.
@ -152,9 +154,8 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// <param name="packageIdentifier">Package Identifier.</param>
/// <param name="packageVersion">Package Version.</param>
/// <param name="packageLocale">Package Locale.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <returns>CosmosPage of Locale.</returns>
Task<ApiDataPage<Locale>> GetLocales(string packageIdentifier, string packageVersion, string packageLocale, IQueryCollection queryParameters);
Task<ApiDataPage<Locale>> GetLocales(string packageIdentifier, string packageVersion, string packageLocale);
/// <summary>
/// Add a Package Manifest.
@ -182,17 +183,19 @@ namespace Microsoft.WinGet.RestSource.Utils.Common
/// Get a Package Manifest based on package identifier and query parameters.
/// </summary>
/// <param name="packageIdentifier">Package Identifier.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <param name="continuationToken">(Optional) Continuation token.</param>
/// <param name="versionFilter">(Optional) Version filter.</param>
/// <param name="channelFilter">(Optional) Channel filter.</param>
/// <param name="marketFilter">(Optional) Market filter.</param>
/// <returns>CosmosPage of Package Manifests.</returns>
Task<ApiDataPage<PackageManifest>> GetPackageManifests(string packageIdentifier, IQueryCollection queryParameters);
Task<ApiDataPage<PackageManifest>> GetPackageManifests(string packageIdentifier, string continuationToken = null, string versionFilter = null, string channelFilter = null, string marketFilter = null);
/// <summary>
/// This will search for manifests based on a manifest search request and a set of query parameters.
/// </summary>
/// <param name="manifestSearchRequest">Manifest Search Request.</param>
/// <param name="headerParameters">Header Parameters.</param>
/// <param name="queryParameters">Query Parameters.</param>
/// <param name="continuationToken">(Optional) Continuation token.</param>
/// <returns>CosmosPage of ManifestSearchResponse.</returns>
Task<ApiDataPage<ManifestSearchResponse>> SearchPackageManifests(ManifestSearchRequest manifestSearchRequest, Dictionary<string, string> headerParameters, IQueryCollection queryParameters);
Task<ApiDataPage<ManifestSearchResponse>> SearchPackageManifests(ManifestSearchRequest manifestSearchRequest, string continuationToken = null);
}
}

Просмотреть файл

@ -0,0 +1,132 @@
// -----------------------------------------------------------------------
// <copyright file="PredicateBuilder.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Utils.Common
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
/// <summary>
/// Enables the efficient, dynamic composition of query predicates.
/// </summary>
public static class PredicateBuilder
{
/// <summary>
/// Creates a predicate that evaluates to true.
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <returns>A constant predicate which always returns true.</returns>
public static Expression<Func<T, bool>> True<T>() => param => true;
/// <summary>
/// Creates a predicate that evaluates to false.
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <returns>A constant predicate which always returns true.</returns>
public static Expression<Func<T, bool>> False<T>() => param => false;
/// <summary>
/// Combines the first predicate with the second using the logical "and".
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <param name="first">The base expression.</param>
/// <param name="second">The expression to combine onto the base.</param>
/// <returns>A new AND-combined predicate.</returns>
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.IsStarted() ? first.Compose(second, Expression.AndAlso) : second;
}
/// <summary>
/// Combines the first predicate with the second using the logical "or".
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <param name="first">The base expression.</param>
/// <param name="second">The expression to combine onto the base.</param>
/// <returns>A new OR-combined predicate.</returns>
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.IsStarted() ? first.Compose(second, Expression.OrElse) : second;
}
/// <summary>
/// Negates the predicate.
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <param name="expression">The base expression.</param>
/// <returns>A new negated expression.</returns>
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
{
var negated = Expression.Not(expression.Body);
return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
}
/// <summary>
/// Determines if the predicate is started.
/// </summary>
/// <typeparam name="T">Type of expression.</typeparam>
/// <param name="expression">Expression to check.</param>
/// <returns>Returns true if predicate has been started (not simply constant true or false), false otherwise.</returns>
public static bool IsStarted<T>(this Expression<Func<T, bool>> expression)
{
return !(expression.Body is ConstantExpression expr && expr.Type == typeof(bool));
}
/// <summary>
/// Create a new predicate with default expression (false).
/// </summary>
/// <typeparam name="T">Type of expression.</typeparam>
/// <returns>A new default expression.</returns>
public static Expression<Func<T, bool>> New<T>()
{
return param => false;
}
/// <summary>
/// Combines the first expression with the second using the specified merge function.
/// </summary>
private static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
{
// zip parameters (map from parameters of second to parameters of first)
var map = first.Parameters
.Select((f, i) => new { f, s = second.Parameters[i] })
.ToDictionary(p => p.s, p => p.f);
// replace parameters in the second lambda expression with the parameters in the first
var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
// create a merged lambda expression with parameters from the first expression
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
}
private class ParameterRebinder : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, ParameterExpression> map;
private ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression p)
{
if (this.map.TryGetValue(p, out ParameterExpression replacement))
{
p = replacement;
}
return base.VisitParameter(p);
}
}
}
}

Просмотреть файл

@ -31,6 +31,9 @@ namespace Microsoft.WinGet.RestSource.Utils.Constants.Enumerations
/// </summary>
public const string Substring = "Substring";
/*********************************
* These are currently unsupported
*********************************
/// <summary>
/// Wildcard.
/// </summary>
@ -45,5 +48,6 @@ namespace Microsoft.WinGet.RestSource.Utils.Constants.Enumerations
/// FuzzySubstring.
/// </summary>
public const string FuzzySubstring = "FuzzySubstring";
*/
}
}

Просмотреть файл

@ -46,6 +46,11 @@ namespace Microsoft.WinGet.RestSource.Utils.Constants.Enumerations
/// </summary>
public const string ProductCode = "ProductCode";
/// <summary>
/// ShortDescription.
/// </summary>
public const string ShortDescription = "ShortDescription";
/// <summary>
/// NormalizedPackageNameAndPublisher.
/// </summary>

Просмотреть файл

@ -5,10 +5,10 @@
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Utils.Models.Schemas
{
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.WinGet.RestSource.Utils.Constants;
using Microsoft.WinGet.RestSource.Utils.Models.Core;
using Microsoft.WinGet.RestSource.Utils.Validators;
using Microsoft.WinGet.RestSource.Utils.Validators.StringValidators;
@ -128,7 +128,7 @@ namespace Microsoft.WinGet.RestSource.Utils.Models.Schemas
/// <inheritdoc />
public override int GetHashCode()
{
return (this.PackageVersion, this.Channel, this.DefaultLocale).GetHashCode();
return HashCode.Combine(this.PackageVersion, this.Channel, this.DefaultLocale);
}
}
}

Просмотреть файл

@ -21,9 +21,12 @@ namespace Microsoft.WinGet.RestSource.Utils.Validators.EnumValidators
MatchType.CaseInsensitive,
MatchType.StartsWith,
MatchType.Substring,
/* These are currently unsupported
MatchType.Wildcard,
MatchType.Fuzzy,
MatchType.FuzzySubstring,
*/
};
/// <summary>

Просмотреть файл

@ -14,7 +14,7 @@ namespace Microsoft.WinGet.RestSource.Utils.Validators.EnumValidators
/// </summary>
public class PackageMatchFieldValidator : ApiEnumValidator
{
private List<string> enumList = new List<string>
private readonly List<string> enumList = new List<string>
{
PackageMatchFields.PackageIdentifier,
PackageMatchFields.PackageName,

Просмотреть файл

@ -34,17 +34,16 @@
</ItemGroup>
<ItemGroup>
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version, most likely when updating to dotnet 5.0 -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.WindowsPackageManager.Utils" Version="0.3.4" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.10" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.2" />
<!-- TODO: Using a preview version of this CosmosDB helper library as there's no stable version yet that supports the v3 CosmosDB SDK.
We'll remove the preview dependency as part of the work to update the LINQ-querying logic.
-->
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="4.0.0-preview2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
# Visual Studio Version 17
VisualStudioVersion = 17.0.31710.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinGet.RestSource", "WinGet.RestSource\WinGet.RestSource.csproj", "{15E4BE9B-2891-410A-A042-47FE838B1AEC}"
EndProject

Просмотреть файл

@ -6,34 +6,26 @@
namespace Microsoft.WinGet.RestSource.Cosmos
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using LinqKit;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.WinGet.RestSource.Utils.Common;
using Microsoft.WinGet.RestSource.Utils.Constants;
using Microsoft.WinGet.RestSource.Utils.Constants.Enumerations;
using Microsoft.WinGet.RestSource.Utils.Exceptions;
using Microsoft.WinGet.RestSource.Utils.Models.Errors;
using Microsoft.WinGet.RestSource.Utils.Models.ExtendedSchemas;
using Microsoft.WinGet.RestSource.Utils.Models.Objects;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
using Microsoft.WinGet.RestSource.Utils.Validators;
using Version = Microsoft.WinGet.RestSource.Utils.Models.Schemas.Version;
/// <summary>
/// Cosmos Data Store.
/// </summary>
public class CosmosDataStore : IApiDataStore
{
private const string ShortDescription = "ShortDescription";
private const int AllElements = -1;
private readonly ICosmosDatabase cosmosDatabase;
@ -49,7 +41,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
PackageMatchFields.PackageFamilyName,
PackageMatchFields.ProductCode,
PackageMatchFields.NormalizedPackageNameAndPublisher,
ShortDescription,
PackageMatchFields.ShortDescription,
};
/// <summary>
@ -67,6 +59,33 @@ namespace Microsoft.WinGet.RestSource.Cosmos
this.log = log;
}
/// <summary>
/// Check if a container exists, and if it doesn't, create it. This will make a read operation, and if the
/// container is not found it will do a create operation.
/// </summary>
/// <param name="throughput">(Optional) The throughput provisioned for a container in measurement of
/// Request Units per second in the Azure Cosmos DB service.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task CreateContainer(int? throughput = null)
{
await this.cosmosDatabase.CreateContainer(throughput);
}
/// <summary>
/// Deletes the Cosmos DB container.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task DeleteContainer()
{
await this.cosmosDatabase.DeleteContainer();
}
/// <inheritdoc />
public async Task<int> Count()
{
return await this.cosmosDatabase.Count<CosmosPackageManifest>();
}
/// <inheritdoc />
public async Task AddPackage(Package package)
{
@ -81,7 +100,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
};
ApiDataValidator.Validate(cosmosDocument.Document);
await this.cosmosDatabase.Add<CosmosPackageManifest>(cosmosDocument);
await this.cosmosDatabase.Add(cosmosDocument);
}
/// <inheritdoc/>
@ -90,7 +109,6 @@ namespace Microsoft.WinGet.RestSource.Cosmos
CosmosDocument<CosmosPackageManifest> cosmosDocument = new CosmosDocument<CosmosPackageManifest>
{
Id = packageIdentifier,
PartitionKey = packageIdentifier,
};
// Delete Document
@ -113,15 +131,8 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
/// <inheritdoc />
public async Task<ApiDataPage<Package>> GetPackages(string packageIdentifier, IQueryCollection queryParameters)
public async Task<ApiDataPage<Package>> GetPackages(string packageIdentifier, string continuationToken = null)
{
// Process Continuation token
string continuationToken = null;
if (queryParameters != null)
{
continuationToken = queryParameters[QueryConstants.ContinuationToken];
}
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Create query options
@ -203,17 +214,8 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
/// <inheritdoc/>
public async Task<ApiDataPage<Version>> GetVersions(string packageIdentifier, string packageVersion, IQueryCollection queryParameters)
public async Task<ApiDataPage<Version>> GetVersions(string packageIdentifier, string packageVersion)
{
// Process Continuation token
string continuationToken = null;
if (queryParameters != null)
{
continuationToken = queryParameters[QueryConstants.ContinuationToken];
}
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Fetch Current Package
CosmosDocument<CosmosPackageManifest> cosmosDocument =
await this.cosmosDatabase.GetByIdAndPartitionKey<CosmosPackageManifest>(packageIdentifier, packageIdentifier);
@ -271,17 +273,8 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
/// <inheritdoc />
public async Task<ApiDataPage<Installer>> GetInstallers(string packageIdentifier, string packageVersion, string installerIdentifier, IQueryCollection queryParameters)
public async Task<ApiDataPage<Installer>> GetInstallers(string packageIdentifier, string packageVersion, string installerIdentifier)
{
// Process Continuation token
string continuationToken = null;
if (queryParameters != null)
{
continuationToken = queryParameters[QueryConstants.ContinuationToken];
}
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Fetch Current Package
CosmosDocument<CosmosPackageManifest> cosmosDocument =
await this.cosmosDatabase.GetByIdAndPartitionKey<CosmosPackageManifest>(packageIdentifier, packageIdentifier);
@ -339,17 +332,8 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
/// <inheritdoc />
public async Task<ApiDataPage<Locale>> GetLocales(string packageIdentifier, string packageVersion, string packageLocale, IQueryCollection queryParameters)
public async Task<ApiDataPage<Locale>> GetLocales(string packageIdentifier, string packageVersion, string packageLocale)
{
// Process Continuation token
string continuationToken = null;
if (queryParameters != null)
{
continuationToken = queryParameters[QueryConstants.ContinuationToken];
}
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Fetch Current Package
CosmosDocument<CosmosPackageManifest> cosmosDocument =
await this.cosmosDatabase.GetByIdAndPartitionKey<CosmosPackageManifest>(packageIdentifier, packageIdentifier);
@ -376,15 +360,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
/// <inheritdoc />
public async Task DeletePackageManifest(string packageIdentifier)
{
// Parse Variables
CosmosDocument<CosmosPackageManifest> cosmosDocument = new CosmosDocument<CosmosPackageManifest>
{
Id = packageIdentifier,
PartitionKey = packageIdentifier,
};
// Delete Document
await this.cosmosDatabase.Delete<CosmosPackageManifest>(cosmosDocument);
await this.DeletePackage(packageIdentifier);
}
/// <inheritdoc />
@ -395,31 +371,16 @@ namespace Microsoft.WinGet.RestSource.Cosmos
{
Document = cosmosPackageManifest,
Id = packageIdentifier,
PartitionKey = packageIdentifier,
};
await this.cosmosDatabase.Update<CosmosPackageManifest>(cosmosDocument);
await this.cosmosDatabase.Update(cosmosDocument);
}
/// <inheritdoc />
public async Task<ApiDataPage<PackageManifest>> GetPackageManifests(string packageIdentifier, IQueryCollection queryParameters)
public async Task<ApiDataPage<PackageManifest>> GetPackageManifests(string packageIdentifier, string continuationToken = null, string versionFilter = null, string channelFilter = null, string marketFilter = null)
{
// Note: GetPackageManifests should use query parameters as predicates when querying all package manifests. Currently, query parameters
// are only exposed for GetPackageManifests with a PackageIdentifier input. Whenever query parameters are exposed to querying all
// package manifests, this method should utilize search predicates to filter on query parameters.
// Process Continuation token
string continuationToken = null;
string versionFilter = null;
string channelFilter = null;
string marketFilter = null;
if (queryParameters != null)
{
continuationToken = queryParameters[QueryConstants.ContinuationToken];
versionFilter = queryParameters[QueryConstants.Version];
channelFilter = queryParameters[QueryConstants.Channel];
marketFilter = queryParameters[QueryConstants.Market];
}
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Create feed options
@ -430,7 +391,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
};
// Get iQueryable
IQueryable<PackageManifest> query = this.cosmosDatabase.GetIQueryable<PackageManifest>(feedOptions);
IQueryable<PackageManifest> query = this.cosmosDatabase.GetIQueryable<PackageManifest>(feedOptions, continuationToken);
if (!string.IsNullOrWhiteSpace(packageIdentifier))
{
@ -474,19 +435,16 @@ namespace Microsoft.WinGet.RestSource.Cosmos
// If Markets object is null or markets do not match filter, exclude them from results.
if (!string.IsNullOrEmpty(marketFilter))
{
this.ApplyMarketFilter(apiDataDocument.Items, marketFilter);
ApplyMarketFilter(apiDataDocument.Items, marketFilter);
}
return apiDataDocument;
}
/// <inheritdoc />
public async Task<ApiDataPage<ManifestSearchResponse>> SearchPackageManifests(ManifestSearchRequest manifestSearchRequest, Dictionary<string, string> headers, IQueryCollection queryParameters)
public async Task<ApiDataPage<ManifestSearchResponse>> SearchPackageManifests(ManifestSearchRequest manifestSearchRequest, string continuationToken = null)
{
// Create Working Set and return
ApiDataPage<ManifestSearchResponse> apiDataPage = new ApiDataPage<ManifestSearchResponse>();
List<CosmosPackageManifest> manifests = new List<CosmosPackageManifest>();
List<ManifestSearchResponse> manifestSearchResponse = new List<ManifestSearchResponse>();
continuationToken = continuationToken != null ? StringEncoder.DecodeContinuationToken(continuationToken) : null;
// Create feed options for inclusion search: -1 so we can get all matches in inclusion, then filter down.
QueryRequestOptions feedOptions = new QueryRequestOptions
@ -495,163 +453,82 @@ namespace Microsoft.WinGet.RestSource.Cosmos
MaxItemCount = AllElements,
};
if (manifestSearchRequest.FetchAllManifests || (manifestSearchRequest.Inclusions == null && manifestSearchRequest.Query == null))
manifestSearchRequest.Inclusions ??= new Utils.Models.Arrays.SearchRequestPackageMatchFilter();
manifestSearchRequest.Filters ??= new Utils.Models.Arrays.SearchRequestPackageMatchFilter();
// Convert Query to inclusions to submit to cosmos
if (manifestSearchRequest.Query != null)
{
IQueryable<CosmosPackageManifest> query = this.cosmosDatabase.GetIQueryable<CosmosPackageManifest>(feedOptions);
FeedIterator<CosmosPackageManifest> documentQuery = query.ToFeedIterator();
ApiDataPage<CosmosPackageManifest> apiDataDocument = await this.cosmosDatabase.GetByDocumentQuery<CosmosPackageManifest>(documentQuery);
manifests.AddRange(apiDataDocument.Items);
manifestSearchRequest.Inclusions.AddRange(this.queryList.Select(q => new SearchRequestPackageMatchFilter()
{
PackageMatchField = q,
RequestMatch = manifestSearchRequest.Query,
}));
}
// Process inclusions
var inclusionsPredicate = new PredicateGenerator();
manifestSearchRequest.Inclusions.ForEach(inclusion => AddConditionIfValid(inclusionsPredicate, inclusion, PredicateOperator.Or));
// Process filters
var filtersPredicate = new PredicateGenerator();
manifestSearchRequest.Filters.ForEach(filter => AddConditionIfValid(filtersPredicate, filter, PredicateOperator.And));
IQueryable<CosmosPackageManifest> query = this.cosmosDatabase.GetIQueryable<CosmosPackageManifest>();
if (inclusionsPredicate.IsStarted() || filtersPredicate.IsStarted())
{
query = query.AsExpandable();
if (inclusionsPredicate.IsStarted())
{
query = query.Where(inclusionsPredicate.Generate(PredicateOperator.Or));
}
if (filtersPredicate.IsStarted())
{
query = query.Where(filtersPredicate.Generate(PredicateOperator.And));
}
}
else
{
// Process Inclusions
Utils.Models.Arrays.SearchRequestPackageMatchFilter inclusions = new Utils.Models.Arrays.SearchRequestPackageMatchFilter();
if (manifestSearchRequest.Inclusions != null)
{
inclusions.AddRange(manifestSearchRequest.Inclusions);
}
// Convert Query to inclusions to submit to cosmos
if (manifestSearchRequest.Query != null)
{
inclusions.AddRange(this.queryList.Select(q => new Utils.Models.Objects.SearchRequestPackageMatchFilter()
{
PackageMatchField = q,
RequestMatch = manifestSearchRequest.Query,
}));
}
// Submit Inclusions to Cosmos
// Due to join limitation on Cosmos - we are submitting each predicate separately.
// TODO: Create a more efficient search - but this will suffice for now for a light weight reference.
if (inclusions.Count > 0)
{
List<Task<ApiDataPage<CosmosPackageManifest>>> taskSet = new List<Task<ApiDataPage<CosmosPackageManifest>>>();
foreach (string packageMatchField in inclusions.Select(inc => inc.PackageMatchField).Distinct())
{
// Create Predicate for search
ExpressionStarter<CosmosPackageManifest> inclusionPredicate = PredicateBuilder.New<CosmosPackageManifest>();
foreach (SearchRequestPackageMatchFilter matchFilter in inclusions.Where(inc => inc.PackageMatchField.Equals(packageMatchField)))
{
// Some package match fields or types might not be supported by current version of search.
// So we will check the supported list before adding any predicates.
if (this.IsPackageMatchFieldSupported(matchFilter.PackageMatchField) &&
this.IsMatchTypeSupported(matchFilter.RequestMatch.MatchType))
{
inclusionPredicate.Or(this.QueryPredicate(matchFilter.PackageMatchField, matchFilter.RequestMatch));
}
}
// Create Document Query
IQueryable<CosmosPackageManifest> query = this.cosmosDatabase.GetIQueryable<CosmosPackageManifest>(feedOptions);
query = query.Where(inclusionPredicate);
FeedIterator<CosmosPackageManifest> documentQuery = query.ToFeedIterator();
// Submit Query to Cosmos
taskSet.Add(Task.Run(() => this.cosmosDatabase.GetByDocumentQuery(documentQuery)));
}
// Wait for Cosmos Queries to complete
await Task.WhenAll(taskSet.ToArray());
// Process Manifests from Cosmos
foreach (Task<ApiDataPage<CosmosPackageManifest>> task in taskSet)
{
manifests.AddRange(task.Result.Items);
}
manifests = manifests.Distinct().ToList();
}
query = query.Where(Utils.Common.PredicateBuilder.True<CosmosPackageManifest>());
}
// Process Filters locally
if (manifestSearchRequest.Filters != null)
{
ExpressionStarter<CosmosPackageManifest> filterPredicate = PredicateBuilder.New<CosmosPackageManifest>();
foreach (SearchRequestPackageMatchFilter matchFilter in manifestSearchRequest.Filters)
{
if (this.IsPackageMatchFieldSupported(matchFilter.PackageMatchField) &&
this.IsMatchTypeSupported(matchFilter.RequestMatch.MatchType))
{
filterPredicate.Or(this.QueryPredicate(matchFilter.PackageMatchField, matchFilter.RequestMatch));
}
}
// Submit Query to Cosmos
var results = await this.cosmosDatabase.GetByDocumentQuery(query, feedOptions, continuationToken);
this.log.LogTrace($"Query used {results.RequestCharge} RUs query: {query}");
manifests = manifests.Where(filterPredicate).ToList();
}
foreach (PackageManifest manifest in manifests)
{
foreach (ManifestSearchResponse response in ManifestSearchResponse.GetSearchVersions(manifest))
{
manifestSearchResponse.Add(response);
}
}
List<ManifestSearchResponse> manifestSearchResponse = results.Items.Distinct().SelectMany(m => ManifestSearchResponse.GetSearchVersions(m)).ToList();
// Consolidate Results
manifestSearchResponse = ManifestSearchResponse.Consolidate(manifestSearchResponse).OrderBy(manifest => manifest.PackageIdentifier).ToList();
// Process results
if (manifestSearchResponse.Count > manifestSearchRequest.MaximumResults && manifestSearchRequest.MaximumResults > 0)
{
manifestSearchResponse = manifestSearchResponse.GetRange(0, manifestSearchRequest.MaximumResults);
}
int maxPageCount = manifestSearchRequest.MaximumResults < FunctionSettingsConstants.MaxResultsPerPage && manifestSearchRequest.MaximumResults > 0
? manifestSearchRequest.MaximumResults
: FunctionSettingsConstants.MaxResultsPerPage;
int totalResults = manifestSearchResponse.Count;
if (totalResults > maxPageCount)
{
// Process Continuation Token
ApiContinuationToken token = null;
if (headers.ContainsKey(HeaderConstants.ContinuationToken))
{
token = Parser.StringParser<ApiContinuationToken>(StringEncoder.DecodeContinuationToken(headers[HeaderConstants.ContinuationToken]));
}
else
{
token = new ApiContinuationToken()
{
Index = 0,
MaxPageSize = maxPageCount,
};
}
// If index miss-match dump results and return no content.
if (token.Index > manifestSearchResponse.Count - 1)
{
manifestSearchResponse = new List<ManifestSearchResponse>();
token = null;
}
else
{
int elementsRemaining = totalResults - token.Index;
int elements = elementsRemaining < token.MaxPageSize ? elementsRemaining : token.MaxPageSize;
manifestSearchResponse = manifestSearchResponse.GetRange(token.Index, elements);
token.Index += elements;
if (token.Index == totalResults)
{
token = null;
}
}
apiDataPage.ContinuationToken = token != null ? StringEncoder.EncodeContinuationToken(FormatJSON.None(token)) : null;
}
apiDataPage.Items = ManifestSearchResponse.Consolidate(manifestSearchResponse.ToList());
// Create Working Set and return
ApiDataPage<ManifestSearchResponse> apiDataPage = new ApiDataPage<ManifestSearchResponse>();
apiDataPage.ContinuationToken = results.ContinuationToken != null ? StringEncoder.EncodeContinuationToken(results.ContinuationToken) : null;
apiDataPage.Items = manifestSearchResponse;
return apiDataPage;
}
private static void AddConditionIfValid(PredicateGenerator predicate, SearchRequestPackageMatchFilter condition, PredicateOperator predicateOperator)
{
// Some package match fields or types might not be supported by current version of search.
// So we will check the supported list before adding any predicates.
if (IsPackageMatchFieldSupported(condition.PackageMatchField) &&
IsMatchTypeSupported(condition.RequestMatch.MatchType))
{
predicate.AddCondition(condition, predicateOperator);
}
}
/// <summary>
/// Method to apply market filter and return results.
/// </summary>
/// <param name="packageManifests">Package manifests on which filter must be applied.</param>
/// <param name="marketFilter">Market filter value.</param>
internal void ApplyMarketFilter(IList<PackageManifest> packageManifests, string marketFilter)
private static void ApplyMarketFilter(IList<PackageManifest> packageManifests, string marketFilter)
{
if (!string.IsNullOrEmpty(marketFilter))
{
@ -697,7 +574,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
}
private bool IsPackageMatchFieldSupported(string packageMatchField)
private static bool IsPackageMatchFieldSupported(string packageMatchField)
{
bool isPackageMatchFieldSupported = false;
@ -718,7 +595,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
return isPackageMatchFieldSupported;
}
private bool IsMatchTypeSupported(string matchType)
private static bool IsMatchTypeSupported(string matchType)
{
bool isMatchTypeSupported = false;
@ -734,121 +611,5 @@ namespace Microsoft.WinGet.RestSource.Cosmos
return isMatchTypeSupported;
}
private Expression<Func<CosmosPackageManifest, bool>> QueryPredicate(string packageMatchField, SearchRequestMatch requestMatch)
{
return (packageMatchField, requestMatch.MatchType) switch
{
(PackageMatchFields.PackageIdentifier, MatchType.Exact) =>
manifest => manifest.PackageIdentifier.Equals(requestMatch.KeyWord),
(PackageMatchFields.PackageIdentifier, MatchType.CaseInsensitive) =>
manifest => manifest.PackageIdentifier.ToLower().Equals(requestMatch.KeyWord.ToLower()),
(PackageMatchFields.PackageIdentifier, MatchType.StartsWith) =>
manifest => manifest.PackageIdentifier.StartsWith(requestMatch.KeyWord),
(PackageMatchFields.PackageIdentifier, MatchType.Substring) =>
manifest => manifest.PackageIdentifier.Contains(requestMatch.KeyWord),
(PackageMatchFields.PackageName, MatchType.Exact) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.PackageName.Equals(requestMatch.KeyWord)),
(PackageMatchFields.PackageName, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.PackageName.ToLower().Equals(requestMatch.KeyWord.ToLower())),
(PackageMatchFields.PackageName, MatchType.StartsWith) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.PackageName.StartsWith(requestMatch.KeyWord)),
(PackageMatchFields.PackageName, MatchType.Substring) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.PackageName.Contains(requestMatch.KeyWord)),
(PackageMatchFields.Moniker, MatchType.Exact) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.Moniker != null && extended.DefaultLocale.Moniker.Equals(requestMatch.KeyWord)),
(PackageMatchFields.Moniker, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.Moniker != null && extended.DefaultLocale.Moniker.ToLower().Equals(requestMatch.KeyWord.ToLower())),
(PackageMatchFields.Moniker, MatchType.StartsWith) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.Moniker != null && extended.DefaultLocale.Moniker.StartsWith(requestMatch.KeyWord)),
(PackageMatchFields.Moniker, MatchType.Substring) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.DefaultLocale.Moniker != null && extended.DefaultLocale.Moniker.Contains(requestMatch.KeyWord)),
(PackageMatchFields.Command, MatchType.Exact) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Commands != null && installer.Commands.Any(command => command.Equals(requestMatch.KeyWord)))),
(PackageMatchFields.Command, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Commands != null && installer.Commands.Any(command => command.ToLower().Equals(requestMatch.KeyWord.ToLower())))),
(PackageMatchFields.Command, MatchType.StartsWith) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Commands != null && installer.Commands.Any(command => command.StartsWith(requestMatch.KeyWord)))),
(PackageMatchFields.Command, MatchType.Substring) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Commands != null && installer.Commands.Any(command => command.Contains(requestMatch.KeyWord)))),
(PackageMatchFields.Tag, MatchType.Exact) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.Tags != null && extended.DefaultLocale.Tags.Any(tag => tag.Equals(requestMatch.KeyWord))) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.Tags != null && locale.Tags.Any(tag => tag.Equals(requestMatch.KeyWord))))),
(PackageMatchFields.Tag, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.Tags != null && extended.DefaultLocale.Tags.Any(tag => tag.ToLower().Equals(requestMatch.KeyWord.ToLower()))) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.Tags != null && locale.Tags.Any(tag => tag.ToLower().Equals(requestMatch.KeyWord.ToLower()))))),
(PackageMatchFields.Tag, MatchType.StartsWith) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.Tags != null && extended.DefaultLocale.Tags.Any(tag => tag.StartsWith(requestMatch.KeyWord))) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.Tags != null && locale.Tags.Any(tag => tag.StartsWith(requestMatch.KeyWord))))),
(PackageMatchFields.Tag, MatchType.Substring) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.Tags != null && extended.DefaultLocale.Tags.Any(tag => tag.Contains(requestMatch.KeyWord))) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.Tags != null && locale.Tags.Any(tag => tag.Contains(requestMatch.KeyWord))))),
(PackageMatchFields.PackageFamilyName, MatchType.Exact) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.PackageFamilyName != null && installer.PackageFamilyName.Equals(requestMatch.KeyWord))),
(PackageMatchFields.PackageFamilyName, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.PackageFamilyName != null && installer.PackageFamilyName.ToLower().Equals(requestMatch.KeyWord.ToLower()))),
(PackageMatchFields.PackageFamilyName, MatchType.StartsWith) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.PackageFamilyName != null && installer.PackageFamilyName.StartsWith(requestMatch.KeyWord))),
(PackageMatchFields.PackageFamilyName, MatchType.Substring) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.PackageFamilyName != null && installer.PackageFamilyName.Contains(requestMatch.KeyWord))),
(PackageMatchFields.ProductCode, MatchType.Exact) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.ProductCode != null && installer.ProductCode.Equals(requestMatch.KeyWord))),
(PackageMatchFields.ProductCode, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.ProductCode != null && installer.ProductCode.ToLower().Equals(requestMatch.KeyWord.ToLower()))),
(PackageMatchFields.ProductCode, MatchType.StartsWith) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.ProductCode != null && installer.ProductCode.StartsWith(requestMatch.KeyWord))),
(PackageMatchFields.ProductCode, MatchType.Substring) =>
manifest => manifest.Versions != null && manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.ProductCode != null && installer.ProductCode.Contains(requestMatch.KeyWord))),
(ShortDescription, MatchType.Exact) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.ShortDescription.Equals(requestMatch.KeyWord)) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.ShortDescription != null && locale.ShortDescription.Equals(requestMatch.KeyWord)))),
(ShortDescription, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.ShortDescription.ToLower().Equals(requestMatch.KeyWord.ToLower())) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.ShortDescription != null && locale.ShortDescription.ToLower().Equals(requestMatch.KeyWord.ToLower())))),
(ShortDescription, MatchType.StartsWith) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.ShortDescription.StartsWith(requestMatch.KeyWord)) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.ShortDescription != null && locale.ShortDescription.StartsWith(requestMatch.KeyWord)))),
(ShortDescription, MatchType.Substring) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.DefaultLocale.ShortDescription.Contains(requestMatch.KeyWord)) || manifest.Versions.Any(extended => extended.Locales != null && extended.Locales.Any(locale => locale.ShortDescription != null && locale.ShortDescription.Contains(requestMatch.KeyWord)))),
(PackageMatchFields.Market, MatchType.Exact) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.AllowedMarkets != null && installer.Markets.AllowedMarkets.Any(market => market.Equals(requestMatch.KeyWord)))) || manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.ExcludedMarkets != null && installer.Markets.ExcludedMarkets.Any(market => !market.Equals(requestMatch.KeyWord))))),
(PackageMatchFields.Market, MatchType.CaseInsensitive) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.AllowedMarkets != null && installer.Markets.AllowedMarkets.Any(market => market.ToLower().Equals(requestMatch.KeyWord.ToLower())))) || manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.ExcludedMarkets != null && installer.Markets.ExcludedMarkets.Any(market => !market.ToLower().Equals(requestMatch.KeyWord.ToLower()))))),
(PackageMatchFields.Market, MatchType.StartsWith) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.AllowedMarkets != null && installer.Markets.AllowedMarkets.Any(market => market.StartsWith(requestMatch.KeyWord)))) || manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.ExcludedMarkets != null && installer.Markets.ExcludedMarkets.Any(market => !market.StartsWith(requestMatch.KeyWord))))),
(PackageMatchFields.Market, MatchType.Substring) =>
manifest => manifest.Versions != null && (manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.AllowedMarkets != null && installer.Markets.AllowedMarkets.Any(market => market.Contains(requestMatch.KeyWord)))) || manifest.Versions.Any(extended => extended.Installers != null && extended.Installers.Any(installer => installer.Markets != null && installer.Markets.ExcludedMarkets != null && installer.Markets.ExcludedMarkets.Any(market => !market.Contains(requestMatch.KeyWord))))),
_ => throw new InvalidArgumentException(new InternalRestError(ErrorConstants.ValidationFailureErrorCode, ErrorConstants.ValidationFailureErrorMessage)),
};
}
}
}

Просмотреть файл

@ -11,10 +11,10 @@ namespace Microsoft.WinGet.RestSource.Cosmos
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.WinGet.RestSource.Utils.Common;
using Microsoft.WinGet.RestSource.Utils.Constants;
using Microsoft.WinGet.RestSource.Utils.Exceptions;
using Newtonsoft.Json;
/// <summary>
/// This class retrieves a database and sets it up if it does not exist.
@ -51,6 +51,27 @@ namespace Microsoft.WinGet.RestSource.Cosmos
this.readWriteContainer = this.readWriteClient.GetContainer(databaseId, containerId);
}
/// <inheritdoc />
public async Task CreateContainer(int? throughput = null)
{
var database = await this.readWriteClient.CreateDatabaseIfNotExistsAsync(this.databaseId);
await database.Database.CreateContainerIfNotExistsAsync(this.containerId, "/id", throughput);
}
/// <inheritdoc />
public async Task DeleteContainer()
{
await this.readWriteContainer.DeleteContainerAsync();
}
/// <inheritdoc />
public async Task<int> Count<T>()
where T : class
{
int count = await this.readOnlyContainer.GetItemLinqQueryable<T>().CountAsync();
return count;
}
/// <inheritdoc />
public async Task Add<T>(CosmosDocument<T> cosmosDocument)
where T : class, ICosmosIdDocument
@ -112,7 +133,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
try
{
var requestOptions = new ItemRequestOptions { IfMatchEtag = cosmosDocument.Etag };
await this.readWriteContainer.ReplaceItemAsync(cosmosDocument.Document, cosmosDocument.Id, requestOptions: requestOptions);
await this.readWriteContainer.ReplaceItemAsync(cosmosDocument.Document, cosmosDocument.Id, new PartitionKey(cosmosDocument.Id), requestOptions: requestOptions);
}
catch (CosmosException cosmosException)
{
@ -125,7 +146,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
}
/// <inheritdoc />
public IQueryable<T> GetIQueryable<T>(QueryRequestOptions requestOptions = null, string continuationToken = null)
public IOrderedQueryable<T> GetIQueryable<T>(QueryRequestOptions requestOptions = null, string continuationToken = null)
where T : class
{
try
@ -135,7 +156,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
requestOptions = new QueryRequestOptions { ResponseContinuationTokenLimitInKb = CosmosConnectionConstants.ResponseContinuationTokenLimitInKb };
}
IQueryable<T> iQueryable = this.readOnlyContainer.GetItemLinqQueryable<T>(continuationToken: continuationToken, requestOptions: requestOptions);
IOrderedQueryable<T> iQueryable = this.readOnlyContainer.GetItemLinqQueryable<T>(true, continuationToken: continuationToken, requestOptions: requestOptions);
return iQueryable;
}
catch (CosmosException cosmosException)
@ -186,6 +207,7 @@ namespace Microsoft.WinGet.RestSource.Cosmos
try
{
FeedResponse<T> response = await documentQuery.ReadNextAsync();
apiDataPage.RequestCharge = response.RequestCharge;
apiDataPage.ContinuationToken = response.ContinuationToken;
apiDataPage.Items = response.ToList();
}
@ -201,5 +223,39 @@ namespace Microsoft.WinGet.RestSource.Cosmos
// Return the model
return apiDataPage;
}
/// <inheritdoc />
public async Task<ApiDataPage<T>> GetByDocumentQuery<T>(IQueryable<T> documentQuery, QueryRequestOptions feedOptions, string continuationToken)
where T : class
{
ApiDataPage<T> apiDataPage = new ApiDataPage<T>();
try
{
string sql = JsonConvert.DeserializeObject<IQueryableSql>(documentQuery.ToString()).Sql;
FeedIterator<T> query = this.readOnlyContainer.GetItemQueryIterator<T>(sql, continuationToken, feedOptions);
FeedResponse<T> response = await query.ReadNextAsync();
apiDataPage.RequestCharge = response.RequestCharge;
apiDataPage.ContinuationToken = response.ContinuationToken;
apiDataPage.Items = response.ToList();
}
catch (CosmosException cosmosException)
{
throw new CosmosDatabaseException(cosmosException);
}
catch (Exception exception)
{
throw new DefaultException(exception);
}
// Return the model
return apiDataPage;
}
private class IQueryableSql
{
[JsonProperty(PropertyName = "query")]
public string Sql { get; set; }
}
}
}

Просмотреть файл

@ -25,9 +25,9 @@ namespace Microsoft.WinGet.RestSource.Cosmos
public string Id { get; set; }
/// <summary>
/// Gets or sets partition Key.
/// Gets partition Key.
/// </summary>
public string PartitionKey { get; set; }
public string PartitionKey => this.Id;
/// <summary>
/// Gets or sets the etag for a document.

Просмотреть файл

@ -2,58 +2,58 @@
// <copyright file="CosmosPackageManifest.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Utils.Models.ExtendedSchemas
{
using Microsoft.WinGet.RestSource.Cosmos;
{
using Microsoft.WinGet.RestSource.Cosmos;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
using Newtonsoft.Json;
/// <summary>
/// This is a manifest, which is an extension of the package core model, and the extended version model.
/// </summary>
public class CosmosPackageManifest : PackageManifest, ICosmosIdDocument
{
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
public CosmosPackageManifest()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
/// <param name="cosmosPackageManifest">manifest.</param>
public CosmosPackageManifest(CosmosPackageManifest cosmosPackageManifest)
: base(cosmosPackageManifest)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
/// <param name="packageManifest">manifest.</param>
public CosmosPackageManifest(PackageManifest packageManifest)
: base(packageManifest)
{
}
/// <summary>
/// Gets ID.
/// </summary>
[JsonProperty("id", Order = 1)]
public string Id => this.PackageIdentifier.ToString();
/// <summary>
/// Converts to a Package Core.
/// </summary>
/// <returns>Package Core.</returns>
public PackageManifest ToManifest()
{
PackageManifest packageManifest = new PackageManifest(this);
return packageManifest;
}
}
}
using Newtonsoft.Json;
/// <summary>
/// This is a manifest, which is an extension of the package core model, and the extended version model.
/// </summary>
public class CosmosPackageManifest : PackageManifest, ICosmosIdDocument
{
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
public CosmosPackageManifest()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
/// <param name="cosmosPackageManifest">manifest.</param>
public CosmosPackageManifest(CosmosPackageManifest cosmosPackageManifest)
: base(cosmosPackageManifest)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CosmosPackageManifest"/> class.
/// </summary>
/// <param name="packageManifest">manifest.</param>
public CosmosPackageManifest(PackageManifest packageManifest)
: base(packageManifest)
{
}
/// <summary>
/// Gets ID.
/// </summary>
[JsonProperty("id", Order = 1)]
public string Id => this.PackageIdentifier.ToString();
/// <summary>
/// Converts to a Package Core.
/// </summary>
/// <returns>Package Core.</returns>
public PackageManifest ToManifest()
{
PackageManifest packageManifest = new PackageManifest(this);
return packageManifest;
}
}
}

Просмотреть файл

@ -16,6 +16,27 @@ namespace Microsoft.WinGet.RestSource.Cosmos
/// </summary>
public interface ICosmosDatabase
{
/// <summary>
/// Check if a container exists, and if it doesn't, create it. This will make a read operation, and if the container is not found it will do a create operation.
/// </summary>
/// <param name="throughput">(Optional) The throughput provisioned for a container in measurement of Request Units per second in the Azure Cosmos DB service.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task CreateContainer(int? throughput = null);
/// <summary>
/// Delete the container from the Azure Cosmos DB service as an asynchronous operation.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteContainer();
/// <summary>
/// Returns the number of items in the Cosmos DB.
/// </summary>
/// <typeparam name="T">Type of the items to count.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<int> Count<T>()
where T : class;
/// <summary>
/// This will add a new document.
/// This will fail if a document already exists that corresponds to the same ID.
@ -55,13 +76,13 @@ namespace Microsoft.WinGet.RestSource.Cosmos
where T : class, ICosmosIdDocument;
/// <summary>
/// This will return an IQueryable for building out document queries.
/// This will return an IOrderedQueryable for building out document queries.
/// </summary>
/// <param name="feedOptions">Feed Options.</param>
/// <param name="continuationToken">(Optional) The continuation token in the Azure Cosmos DB service.</param>
/// <typeparam name="T">Document Type.</typeparam>
/// <returns>IQueryable.</returns>
IQueryable<T> GetIQueryable<T>(QueryRequestOptions feedOptions = null, string continuationToken = null)
IOrderedQueryable<T> GetIQueryable<T>(QueryRequestOptions feedOptions = null, string continuationToken = null)
where T : class;
/// <summary>
@ -82,5 +103,16 @@ namespace Microsoft.WinGet.RestSource.Cosmos
/// <returns>Document.</returns>
Task<ApiDataPage<T>> GetByDocumentQuery<T>(FeedIterator<T> documentQuery)
where T : class;
/// <summary>
/// This will retrieve a document by document query.
/// </summary>
/// <param name="documentQuery">Document Query.</param>
/// <param name="feedOptions">Feed Options.</param>
/// <param name="continuationToken">(Optional) The continuation token in the Azure Cosmos DB service.</param>
/// <typeparam name="T">Document Type.</typeparam>
/// <returns>Document.</returns>
Task<ApiDataPage<T>> GetByDocumentQuery<T>(IQueryable<T> documentQuery, QueryRequestOptions feedOptions, string continuationToken)
where T : class;
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// -----------------------------------------------------------------------
// <copyright file="PredicateGenerator.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.WinGet.RestSource.Cosmos
{
using System;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.WinGet.RestSource.Utils.Common;
using Microsoft.WinGet.RestSource.Utils.Constants.Enumerations;
using Microsoft.WinGet.RestSource.Utils.Models.ExtendedSchemas;
using Microsoft.WinGet.RestSource.Utils.Models.Objects;
using Microsoft.WinGet.RestSource.Utils.Models.Schemas;
using PredicateOperator = LinqKit.PredicateOperator;
/// <summary>
/// Expression predicate generator that is safe for usage with Cosmos DB.
/// </summary>
public class PredicateGenerator
{
private Expression<Func<CosmosPackageManifest, bool>> rootPredicate;
private Expression<Func<VersionExtended, bool>> versionPredicate;
private Expression<Func<Installer, bool>> installerPredicate;
/// <summary>
/// Initializes a new instance of the <see cref="PredicateGenerator"/> class.
/// </summary>
public PredicateGenerator()
{
this.rootPredicate = PredicateBuilder.False<CosmosPackageManifest>();
this.versionPredicate = PredicateBuilder.False<VersionExtended>();
this.installerPredicate = PredicateBuilder.False<Installer>();
}
/// <summary>
/// Adds a condition to this predicate based on specified condition and operator.
/// </summary>
/// <param name="condition">Condition to add to predicate.</param>
/// <param name="predicateOperator">Operator to apply to new condition.</param>
public void AddCondition(SearchRequestPackageMatchFilter condition, PredicateOperator predicateOperator)
{
var op = GetOperator(condition.RequestMatch.MatchType);
if (GetRootClause(condition, op, out Expression<Func<CosmosPackageManifest, bool>> rootClause))
{
this.rootPredicate = predicateOperator == PredicateOperator.Or ? this.rootPredicate.Or(rootClause) : this.rootPredicate.And(rootClause);
}
else if (GetVersionClause(condition, op, out Expression<Func<VersionExtended, bool>> versionClause))
{
this.versionPredicate = predicateOperator == PredicateOperator.Or ? this.versionPredicate.Or(versionClause) : this.versionPredicate.And(versionClause);
}
else if (GetInstallerClause(condition, op, out Expression<Func<Installer, bool>> installerClause))
{
this.installerPredicate = predicateOperator == PredicateOperator.Or ? this.installerPredicate.Or(installerClause) : this.installerPredicate.And(installerClause);
}
}
/// <summary>
/// Merge all sub-predicates into a single predicate, simplifying where possible.
/// </summary>
/// <param name="predicateOperator">Operator to use for merging.</param>
/// <returns>Merged root predicate.</returns>
public Expression<Func<CosmosPackageManifest, bool>> Generate(PredicateOperator predicateOperator)
{
if (this.versionPredicate.IsStarted() || this.installerPredicate.IsStarted())
{
if (this.installerPredicate.IsStarted())
{
Expression<Func<VersionExtended, bool>> installerClause = v => v.Installers.Any(this.installerPredicate.Compile());
this.versionPredicate = predicateOperator == PredicateOperator.Or ? this.versionPredicate.Or(installerClause) : this.versionPredicate.And(installerClause);
}
Expression<Func<CosmosPackageManifest, bool>> versionClause = m => m.Versions.Any(this.versionPredicate.Compile());
this.rootPredicate = predicateOperator == PredicateOperator.Or ? this.rootPredicate.Or(versionClause) : this.rootPredicate.And(versionClause);
}
return this.rootPredicate;
}
/// <summary>
/// Determine if the predicate is the default predicate.
/// </summary>
/// <returns>Returns true if the predicate is not default, false otherwise.</returns>
public bool IsStarted()
{
return this.rootPredicate.IsStarted() || this.versionPredicate.IsStarted() || this.installerPredicate.IsStarted();
}
private static Expression<Func<string, string, bool>> GetOperator(string matchType)
{
return matchType switch
{
MatchType.Exact => (field, keyword) => field.Equals(keyword),
MatchType.CaseInsensitive => (field, keyword) => field.Equals(keyword, StringComparison.OrdinalIgnoreCase),
MatchType.StartsWith => (field, keyword) => field.StartsWith(keyword),
MatchType.Substring => (field, keyword) => field.Contains(keyword),
_ => null,
};
}
private static bool GetRootClause(SearchRequestPackageMatchFilter filter, Expression<Func<string, string, bool>> op, out Expression<Func<CosmosPackageManifest, bool>> clause)
{
string keyword = filter.RequestMatch.KeyWord;
clause = filter.PackageMatchField switch
{
PackageMatchFields.PackageIdentifier => m => LinqKit.Extensions.Invoke(op, m.PackageIdentifier, keyword),
_ => null,
};
return clause != null;
}
private static bool GetVersionClause(SearchRequestPackageMatchFilter filter, Expression<Func<string, string, bool>> op, out Expression<Func<VersionExtended, bool>> clause)
{
string keyword = filter.RequestMatch.KeyWord;
clause = filter.PackageMatchField switch
{
PackageMatchFields.PackageName => v => LinqKit.Extensions.Invoke(op, v.DefaultLocale.PackageName, keyword),
PackageMatchFields.Moniker => v => LinqKit.Extensions.Invoke(op, v.DefaultLocale.Moniker, keyword),
PackageMatchFields.ShortDescription => v => LinqKit.Extensions.Invoke(op, v.DefaultLocale.ShortDescription, keyword),
PackageMatchFields.Tag => v => v.DefaultLocale.Tags.Any(t => LinqKit.Extensions.Invoke(op, t, keyword)),
_ => null,
};
return clause != null;
}
private static bool GetInstallerClause(SearchRequestPackageMatchFilter filter, Expression<Func<string, string, bool>> op, out Expression<Func<Installer, bool>> clause)
{
string keyword = filter.RequestMatch.KeyWord;
clause = filter.PackageMatchField switch
{
PackageMatchFields.PackageFamilyName => i => LinqKit.Extensions.Invoke(op, i.PackageFamilyName, keyword),
PackageMatchFields.ProductCode => i => LinqKit.Extensions.Invoke(op, i.ProductCode, keyword),
PackageMatchFields.Command => i => i.Commands.Any(c => LinqKit.Extensions.Invoke(op, c, keyword)),
_ => null,
};
return clause != null;
}
}
}

Просмотреть файл

@ -1,50 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.WinGet.RestSource</AssemblyName>
<RootNamespace>Microsoft.WinGet.RestSource</RootNamespace>
<OutputType>Library</OutputType>
<LangVersion>8</LangVersion>
</PropertyGroup>
<PropertyGroup>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<NoWarn>1701;1702;NU1701</NoWarn>
<DocumentationFile>$(SolutionDir)WinGet.RestSource\Microsoft.WinGet.RestSource.Documentation.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<WarningsAsErrors />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LinqKit" Version="1.1.24" />
<!-- TODO: Using a preview version of this CosmosDB helper library as there's no stable version yet that supports the v3 CosmosDB SDK.
We'll remove the preview dependency as part of the work to update the LINQ-querying logic.
-->
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="4.0.0-preview2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.10" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.WinGet.RestSource</AssemblyName>
<RootNamespace>Microsoft.WinGet.RestSource</RootNamespace>
<OutputType>Library</OutputType>
<LangVersion>8</LangVersion>
</PropertyGroup>
<PropertyGroup>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<NoWarn>1701;1702;NU1701</NoWarn>
<DocumentationFile>$(SolutionDir)WinGet.RestSource\Microsoft.WinGet.RestSource.Documentation.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<WarningsAsErrors />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<!-- Component Governance fix. Remove when dependency resolving correctly picks up new version, most likely when updating to dotnet 5.0 -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
<PackageReference Include="LinqKit" Version="1.1.26" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.22.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.10" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinGet.RestSource.Utils\WinGet.RestSource.Utils.csproj" />
</ItemGroup>
</Project>
</Project>

Просмотреть файл

@ -51,6 +51,11 @@
Gets or sets continuation Token.
</summary>
</member>
<member name="P:Microsoft.WinGet.RestSource.Utils.Common.ApiDataPage`1.RequestCharge">
<summary>
Gets or sets the request charge for this request from the Azure Cosmos DB service.
</summary>
</member>
<member name="T:Microsoft.WinGet.RestSource.Utils.Common.FormatJSON">
<summary>
Class that contains JSON helpers for printing and exporting data.
@ -103,6 +108,12 @@
This provides an interface for IApiDataStore.
</summary>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.Count">
<summary>
Returns the count of items in the data store.
</summary>
<returns>The number if items in the data store.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.AddPackage(Microsoft.WinGet.RestSource.Utils.Models.Schemas.Package)">
<summary>
This will add a package to the data store.
@ -125,12 +136,12 @@
<param name="package">Package.</param>
<returns>Task.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetPackages(System.String,Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetPackages(System.String,System.String)">
<summary>
This will retrieve a set of packages based on a package identifier and a continuation token.
</summary>
<param name="packageIdentifier">Package Identifier.</param>
<param name="queryParameters">Query Parameters.</param>
<param name="continuationToken">(Optional) Continuation token.</param>
<returns>CosmosPage of Package Manifests.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.AddVersion(System.String,Microsoft.WinGet.RestSource.Utils.Models.Schemas.Version)">
@ -158,13 +169,12 @@
<param name="version">Version.</param>
<returns>Task.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetVersions(System.String,System.String,Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetVersions(System.String,System.String)">
<summary>
This will get a set of versions based on the package identifier, package version, and any continuation token.
</summary>
<param name="packageIdentifier">Package Identifier.</param>
<param name="packageVersion">Package Version.</param>
<param name="queryParameters">Query Parameters.</param>
<returns>CosmosPage of Version.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.AddInstaller(System.String,System.String,Microsoft.WinGet.RestSource.Utils.Models.Schemas.Installer)">
@ -195,14 +205,13 @@
<param name="installer">Installer.</param>
<returns>Task.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetInstallers(System.String,System.String,System.String,Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetInstallers(System.String,System.String,System.String)">
<summary>
This will get a set of Installers based on the package identifier, package version, installer identifier, and any continuation token.
</summary>
<param name="packageIdentifier">Package Identifier.</param>
<param name="packageVersion">Package Version.</param>
<param name="installerIdentifier">Installer Identifier.</param>
<param name="queryParameters">Query Parameters.</param>
<returns>CosmosPage of Installer.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.AddLocale(System.String,System.String,Microsoft.WinGet.RestSource.Utils.Models.Schemas.Locale)">
@ -233,14 +242,13 @@
<param name="locale">Locale.</param>
<returns>Task.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetLocales(System.String,System.String,System.String,Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetLocales(System.String,System.String,System.String)">
<summary>
This will get a set of Installers based on the package identifier, package version, installer identifier, and any continuation token.
</summary>
<param name="packageIdentifier">Package Identifier.</param>
<param name="packageVersion">Package Version.</param>
<param name="packageLocale">Package Locale.</param>
<param name="queryParameters">Query Parameters.</param>
<returns>CosmosPage of Locale.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.AddPackageManifest(Microsoft.WinGet.RestSource.Utils.Models.Schemas.PackageManifest)">
@ -265,21 +273,23 @@
<param name="packageManifest">Package Manifest.</param>
<returns>Task.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetPackageManifests(System.String,Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.GetPackageManifests(System.String,System.String,System.String,System.String,System.String)">
<summary>
Get a Package Manifest based on package identifier and query parameters.
</summary>
<param name="packageIdentifier">Package Identifier.</param>
<param name="queryParameters">Query Parameters.</param>
<param name="continuationToken">(Optional) Continuation token.</param>
<param name="versionFilter">(Optional) Version filter.</param>
<param name="channelFilter">(Optional) Channel filter.</param>
<param name="marketFilter">(Optional) Market filter.</param>
<returns>CosmosPage of Package Manifests.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.SearchPackageManifests(Microsoft.WinGet.RestSource.Utils.Models.Schemas.ManifestSearchRequest,System.Collections.Generic.Dictionary{System.String,System.String},Microsoft.AspNetCore.Http.IQueryCollection)">
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.IApiDataStore.SearchPackageManifests(Microsoft.WinGet.RestSource.Utils.Models.Schemas.ManifestSearchRequest,System.String)">
<summary>
This will search for manifests based on a manifest search request and a set of query parameters.
</summary>
<param name="manifestSearchRequest">Manifest Search Request.</param>
<param name="headerParameters">Header Parameters.</param>
<param name="queryParameters">Query Parameters.</param>
<param name="continuationToken">(Optional) Continuation token.</param>
<returns>CosmosPage of ManifestSearchResponse.</returns>
</member>
<member name="T:Microsoft.WinGet.RestSource.Utils.Common.Parser">
@ -305,6 +315,71 @@
<typeparam name="T">Object to return.</typeparam>
<returns>Object.</returns>
</member>
<member name="T:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder">
<summary>
Enables the efficient, dynamic composition of query predicates.
</summary>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.True``1">
<summary>
Creates a predicate that evaluates to true.
</summary>
<typeparam name="T">The expression type.</typeparam>
<returns>A constant predicate which always returns true.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.False``1">
<summary>
Creates a predicate that evaluates to false.
</summary>
<typeparam name="T">The expression type.</typeparam>
<returns>A constant predicate which always returns true.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.And``1(System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}},System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}})">
<summary>
Combines the first predicate with the second using the logical "and".
</summary>
<typeparam name="T">The expression type.</typeparam>
<param name="first">The base expression.</param>
<param name="second">The expression to combine onto the base.</param>
<returns>A new AND-combined predicate.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.Or``1(System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}},System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}})">
<summary>
Combines the first predicate with the second using the logical "or".
</summary>
<typeparam name="T">The expression type.</typeparam>
<param name="first">The base expression.</param>
<param name="second">The expression to combine onto the base.</param>
<returns>A new OR-combined predicate.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.Not``1(System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}})">
<summary>
Negates the predicate.
</summary>
<typeparam name="T">The expression type.</typeparam>
<param name="expression">The base expression.</param>
<returns>A new negated expression.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.IsStarted``1(System.Linq.Expressions.Expression{System.Func{``0,System.Boolean}})">
<summary>
Determines if the predicate is started.
</summary>
<typeparam name="T">Type of expression.</typeparam>
<param name="expression">Expression to check.</param>
<returns>Returns true if predicate has been started (not simply constant true or false), false otherwise.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.New``1">
<summary>
Create a new predicate with default expression (false).
</summary>
<typeparam name="T">Type of expression.</typeparam>
<returns>A new default expression.</returns>
</member>
<member name="M:Microsoft.WinGet.RestSource.Utils.Common.PredicateBuilder.Compose``1(System.Linq.Expressions.Expression{``0},System.Linq.Expressions.Expression{``0},System.Func{System.Linq.Expressions.Expression,System.Linq.Expressions.Expression,System.Linq.Expressions.Expression})">
<summary>
Combines the first expression with the second using the specified merge function.
</summary>
</member>
<member name="T:Microsoft.WinGet.RestSource.Utils.Common.StringEncoder">
<summary>
String Encoder.
@ -523,21 +598,6 @@
Substring.
</summary>
</member>
<member name="F:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.MatchType.Wildcard">
<summary>
Wildcard.
</summary>
</member>
<member name="F:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.MatchType.Fuzzy">
<summary>
Fuzzy.
</summary>
</member>
<member name="F:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.MatchType.FuzzySubstring">
<summary>
FuzzySubstring.
</summary>
</member>
<member name="T:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.PackageMatchFields">
<summary>
Package Match Field Constants.
@ -578,6 +638,11 @@
ProductCode.
</summary>
</member>
<member name="F:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.PackageMatchFields.ShortDescription">
<summary>
ShortDescription.
</summary>
</member>
<member name="F:Microsoft.WinGet.RestSource.Utils.Constants.Enumerations.PackageMatchFields.NormalizedPackageNameAndPublisher">
<summary>
NormalizedPackageNameAndPublisher.