diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 0000000..f3ff562
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,122 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Build Schema",
+ "$ref": "#/definitions/build",
+ "definitions": {
+ "build": {
+ "type": "object",
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "GitHubAccessToken": {
+ "type": "string",
+ "description": "GitHub access token used for creating a new or updating an existing release"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "type": "string",
+ "description": "Host for execution. Default is 'automatic'",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "NuGetApiKey": {
+ "type": "string",
+ "description": "NuGet API key used to pushing the Sdk NuGet package"
+ },
+ "NuGetSource": {
+ "type": "string",
+ "description": "NuGet source used for pushing the Sdk NuGet package. Default is NuGet.org"
+ },
+ "Partition": {
+ "type": "string",
+ "description": "Partition to use on CI"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "Restore"
+ ]
+ }
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "Restore"
+ ]
+ }
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "enum": [
+ "Minimal",
+ "Normal",
+ "Quiet",
+ "Verbose"
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
new file mode 100644
index 0000000..becac29
--- /dev/null
+++ b/.nuke/parameters.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "./build.schema.json",
+ "Solution": "Sknet.Openiddict.LiteDB.sln"
+}
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..30ed802
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,13 @@
+=======================================================================
+Sknet.OpenIddict.LiteDB
+Copyright (c) 2022 Steven Kuhn and contributors. All rights reserved.
+=======================================================================
+
+This product is based on the OpenIddict (openiddict/openiddict-core) project developed by Kévin Chalet and its contributors at https://github.com/openiddict/openiddict-core.
+
+=======================================================================
+OpenIddict
+© Kévin Chalet. All rights reserved.
+=======================================================================
+
+Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0). See https://github.com/openiddict/openiddict-core for more information concerning the license and the contributors participating to this project.
\ No newline at end of file
diff --git a/Sknet.Openiddict.LiteDB.sln b/Sknet.Openiddict.LiteDB.sln
new file mode 100644
index 0000000..8c02c46
--- /dev/null
+++ b/Sknet.Openiddict.LiteDB.sln
@@ -0,0 +1,53 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.3.32825.248
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CB8DF41A-EFAE-4CDD-A8CB-558CF2F50E5F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sknet.OpenIddict.LiteDB", "src\Sknet.OpenIddict.LiteDB\Sknet.OpenIddict.LiteDB.csproj", "{02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sknet.OpenIddict.LiteDB.Models", "src\Sknet.OpenIddict.LiteDB.Models\Sknet.OpenIddict.LiteDB.Models.csproj", "{198BFA54-A239-4149-9A02-0259761A2127}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{217F1279-2978-4C25-B60F-0B65BA2E3B00}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Build", "build\Build.csproj", "{B2746FD1-1277-4BFC-954A-84753B12A028}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sknet.OpenIddict.LiteDB.Tests", "test\Sknet.OpenIddict.LiteDB.Tests\Sknet.OpenIddict.LiteDB.Tests.csproj", "{4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {198BFA54-A239-4149-9A02-0259761A2127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {198BFA54-A239-4149-9A02-0259761A2127}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {198BFA54-A239-4149-9A02-0259761A2127}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {198BFA54-A239-4149-9A02-0259761A2127}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2746FD1-1277-4BFC-954A-84753B12A028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2746FD1-1277-4BFC-954A-84753B12A028}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {02AC77ED-78FA-46ED-91D9-B9FA403EF7ED} = {649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B}
+ {198BFA54-A239-4149-9A02-0259761A2127} = {649E750E-7C5F-4D27-BE9A-CFEB4EA0C12B}
+ {B2746FD1-1277-4BFC-954A-84753B12A028} = {217F1279-2978-4C25-B60F-0B65BA2E3B00}
+ {4D1B3900-4C81-4E9F-BB32-3FA7A8ECCF1D} = {CB8DF41A-EFAE-4CDD-A8CB-558CF2F50E5F}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6ADAF88E-F6DC-44EF-957A-79D15C271767}
+ EndGlobalSection
+EndGlobal
diff --git a/Sknet.Openiddict.LiteDB.v3.ncrunchsolution b/Sknet.Openiddict.LiteDB.v3.ncrunchsolution
new file mode 100644
index 0000000..10420ac
--- /dev/null
+++ b/Sknet.Openiddict.LiteDB.v3.ncrunchsolution
@@ -0,0 +1,6 @@
+
+
+ True
+ True
+
+
\ No newline at end of file
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 0000000..b08cc59
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..8833a53
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,69 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+ [string[]]$BuildArguments
+)
+
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
+
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+$BuildProjectFile = "$PSScriptRoot\build\Build.csproj"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
+
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
+$DotNetChannel = "Current"
+
+$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+$env:DOTNET_MULTILEVEL_LOOKUP = 0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+ & $cmd
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
+ $(dotnet --version) -and $LASTEXITCODE -eq 0) {
+ $env:DOTNET_EXE = (Get-Command "dotnet").Path
+}
+else {
+ # Download install script
+ $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+ New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+ # If global.json exists, load expected version
+ if (Test-Path $DotNetGlobalFile) {
+ $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+ if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+ $DotNetVersion = $DotNetGlobal.sdk.version
+ }
+ }
+
+ # Install by channel or version
+ $DotNetDirectory = "$TempDirectory\dotnet-win"
+ if (!(Test-Path variable:DotNetVersion)) {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+ } else {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+ }
+ $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+}
+
+Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
+
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..10c6ced
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+bash --version 2>&1 | head -n 1
+
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+BUILD_PROJECT_FILE="$SCRIPT_DIR/build/Build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="Current"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+export DOTNET_MULTILEVEL_LOOKUP=0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+ perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+ export DOTNET_EXE="$(command -v dotnet)"
+else
+ # Download install script
+ DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+ mkdir -p "$TEMP_DIRECTORY"
+ curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+ chmod +x "$DOTNET_INSTALL_FILE"
+
+ # If global.json exists, load expected version
+ if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+ DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+ if [[ "$DOTNET_VERSION" == "" ]]; then
+ unset DOTNET_VERSION
+ fi
+ fi
+
+ # Install by channel or version
+ DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+ if [[ -z ${DOTNET_VERSION+x} ]]; then
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+ else
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+ fi
+ export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+fi
+
+echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
diff --git a/build/.editorconfig b/build/.editorconfig
new file mode 100644
index 0000000..31e43dc
--- /dev/null
+++ b/build/.editorconfig
@@ -0,0 +1,11 @@
+[*.cs]
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_require_accessibility_modifiers = never:warning
+
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
diff --git a/build/Build.cs b/build/Build.cs
new file mode 100644
index 0000000..07c635a
--- /dev/null
+++ b/build/Build.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Linq;
+using Newtonsoft.Json;
+using Nuke.Common;
+using Nuke.Common.CI;
+class Build : NukeBuild
+{
+ /// Support plugins are available for:
+ /// - JetBrains ReSharper https://nuke.build/resharper
+ /// - JetBrains Rider https://nuke.build/rider
+ /// - Microsoft VisualStudio https://nuke.build/visualstudio
+ /// - Microsoft VSCode https://nuke.build/vscode
+
+ public static int Main () => Execute(x => x.Compile);
+
+ [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+ readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+ [Parameter("GitHub access token used for creating a new or updating an existing release.")]
+ readonly string GitHubAccessToken;
+
+ [Parameter("NuGet source used for pushing the Sdk NuGet package. Default is NuGet.org.")]
+ readonly string NuGetSource = "https://api.nuget.org/v3/index.json";
+
+ [Parameter("NuGet API key used to pushing the Sdk NuGet package.")]
+ readonly string NuGetApiKey;
+
+ [Solution] readonly Solution Solution;
+ [GitRepository] readonly GitRepository GitRepository;
+ [GitVersion(Framework = "net6.0", NoFetch = true)] readonly GitVersion GitVersion;
+
+ static AbsolutePath SourceDirectory => RootDirectory / "src";
+ static AbsolutePath TestsDirectory => RootDirectory / "test";
+ static AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts";
+
+ const string GitHubRepositoryName = "InRuleGitStorage";
+ const string GitHubRepositoryOwner = "openiddict-litedb";
+
+ readonly string[] NuGetRestoreSources = new[] {
+ "https://api.nuget.org/v3/index.json"
+ };
+
+ protected override void OnBuildInitialized()
+ {
+ Log.Information($"GitVersion settings:\n{JsonConvert.SerializeObject(GitVersion, Formatting.Indented)}");
+ }
+
+ Target Clean => _ => _
+ .Before(Restore)
+ .Executes(() =>
+ {
+ Log.Debug("Deleting all bin/obj directories...");
+ SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
+ TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
+
+ Log.Debug("Cleaning artifacts directory...");
+ EnsureCleanDirectory(ArtifactsDirectory);
+
+ Log.Debug("Deleting test results directories...");
+ TestsDirectory.GlobDirectories("**/TestResults").ForEach(DeleteDirectory);
+ });
+
+ Target Restore => _ => _
+ .Executes(() =>
+ {
+ Log.Debug("Restoring NuGet packages for solution...");
+ DotNetRestore(s => s
+ .SetProjectFile(Solution)
+ .SetSources(NuGetRestoreSources));
+ });
+
+ Target Compile => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ Log.Debug("Compiling solution...");
+ DotNetBuild(s => s
+ .SetProjectFile(Solution)
+ .SetVersion(GitVersion.FullSemVer)
+ .SetAssemblyVersion(GitVersion.AssemblySemVer)
+ .SetFileVersion(GitVersion.AssemblySemFileVer)
+ .SetInformationalVersion(GitVersion.InformationalVersion)
+ .SetProperty("RepositoryBranch", GitVersion.BranchName)
+ .SetProperty("RepositoryCommit", GitVersion.Sha)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+}
diff --git a/build/Build.csproj b/build/Build.csproj
new file mode 100644
index 0000000..560b64f
--- /dev/null
+++ b/build/Build.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net6.0
+
+ CS0649;CS0169
+ ..
+ ..
+ 1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/Build.csproj.DotSettings b/build/Build.csproj.DotSettings
new file mode 100644
index 0000000..c8947fc
--- /dev/null
+++ b/build/Build.csproj.DotSettings
@@ -0,0 +1,26 @@
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ Implicit
+ Implicit
+ ExpressionBody
+ 0
+ NEXT_LINE
+ True
+ False
+ 120
+ IF_OWNER_IS_SINGLE_LINE
+ WRAP_IF_LONG
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/build/Configuration.cs b/build/Configuration.cs
new file mode 100644
index 0000000..ad4e5ca
--- /dev/null
+++ b/build/Configuration.cs
@@ -0,0 +1,11 @@
+[TypeConverter(typeof(TypeConverter))]
+public class Configuration : Enumeration
+{
+ public static Configuration Debug = new Configuration { Value = nameof(Debug) };
+ public static Configuration Release = new Configuration { Value = nameof(Release) };
+
+ public static implicit operator string(Configuration configuration)
+ {
+ return configuration.Value;
+ }
+}
diff --git a/build/Directory.Build.props b/build/Directory.Build.props
new file mode 100644
index 0000000..e147d63
--- /dev/null
+++ b/build/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets
new file mode 100644
index 0000000..2532609
--- /dev/null
+++ b/build/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/build/Usings.cs b/build/Usings.cs
new file mode 100644
index 0000000..eb88c1b
--- /dev/null
+++ b/build/Usings.cs
@@ -0,0 +1,26 @@
+global using System;
+global using System.ComponentModel;
+global using System.IO;
+global using System.Linq;
+
+global using Newtonsoft.Json;
+global using Serilog;
+global using Nuke.Common;
+global using Nuke.Common.CI;
+global using Nuke.Common.Execution;
+global using Nuke.Common.Git;
+global using Nuke.Common.IO;
+global using Nuke.Common.ProjectModel;
+global using Nuke.Common.Tooling;
+global using Nuke.Common.Tools.DotNet;
+global using Nuke.Common.Tools.GitVersion;
+global using Nuke.Common.Tools.MSBuild;
+global using Nuke.Common.Tools.NuGet;
+global using Nuke.Common.Utilities.Collections;
+
+global using static Nuke.Common.EnvironmentInfo;
+global using static Nuke.Common.IO.CompressionTasks;
+global using static Nuke.Common.IO.FileSystemTasks;
+global using static Nuke.Common.IO.PathConstruction;
+global using static Nuke.Common.Tools.DotNet.DotNetTasks;
+global using static Nuke.Common.Tools.NuGet.NuGetTasks;
\ No newline at end of file
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..94fdf50
Binary files /dev/null and b/icon.png differ
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs
new file mode 100644
index 0000000..d61a228
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBApplication.cs
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB.Models;
+
+///
+/// Represents an OpenIddict application.
+///
+[DebuggerDisplay("Id = {Id.ToString(),nq} ; ClientId = {ClientId,nq} ; Type = {Type,nq}")]
+public class OpenIddictLiteDBApplication
+{
+ ///
+ /// Gets or sets the client identifier associated with the current application.
+ ///
+ [BsonField("client_id")]
+ public virtual string? ClientId { get; set; }
+
+ ///
+ /// Gets or sets the client secret associated with the current application.
+ /// Note: depending on the application manager used to create this instance,
+ /// this property may be hashed or encrypted for security reasons.
+ ///
+ [BsonField("client_secret")]
+ public virtual string? ClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the concurrency token.
+ ///
+ [BsonField("concurrency_token")]
+ public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// Gets or sets the consent type associated with the current application.
+ ///
+ [BsonField("consent_type")]
+ public virtual string? ConsentType { get; set; }
+
+ ///
+ /// Gets or sets the display name associated with the current application.
+ ///
+ [BsonField("display_name")]
+ public virtual string? DisplayName { get; set; }
+
+ ///
+ /// Gets or sets the localized display names associated with the current application.
+ ///
+ [BsonField("display_names")]
+ public virtual IReadOnlyDictionary? DisplayNames { get; set; }
+ = ImmutableDictionary.Create();
+
+ ///
+ /// Gets or sets the unique identifier associated with the current application.
+ ///
+ [BsonId]
+ public virtual ObjectId Id { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the permissions associated with the current application.
+ ///
+ [BsonField("permissions")]
+ public virtual IReadOnlyList? Permissions { get; set; } = ImmutableList.Create();
+
+ ///
+ /// Gets or sets the logout callback URLs associated with the current application.
+ ///
+ [BsonField("post_logout_redirect_uris")]
+ public virtual IReadOnlyList? PostLogoutRedirectUris { get; set; } = ImmutableList.Create();
+
+ ///
+ /// Gets or sets the additional properties associated with the current application.
+ ///
+ [BsonField("properties")]
+ public virtual IReadOnlyDictionary? Properties { get; set; }
+
+ ///
+ /// Gets or sets the callback URLs associated with the current application.
+ ///
+ [BsonField("redirect_uris")]
+ public virtual IReadOnlyList? RedirectUris { get; set; } = ImmutableList.Create();
+
+ ///
+ /// Gets or sets the requirements associated with the current application.
+ ///
+ [BsonField("requirements")]
+ public virtual IReadOnlyList? Requirements { get; set; } = ImmutableList.Create();
+
+ ///
+ /// Gets or sets the application type
+ /// associated with the current application.
+ ///
+ [BsonField("type")]
+ public virtual string? Type { get; set; }
+}
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs
new file mode 100644
index 0000000..82c13d6
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBAuthorization.cs
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB.Models;
+
+///
+/// Represents an OpenIddict authorization.
+///
+[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")]
+public class OpenIddictLiteDBAuthorization
+{
+ ///
+ /// Gets or sets the identifier of the application associated with the current authorization.
+ ///
+ [BsonField("application_id")]
+ public virtual ObjectId ApplicationId { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the concurrency token.
+ ///
+ [BsonField("concurrency_token")]
+ public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// Gets or sets the UTC creation date of the current authorization.
+ ///
+ [BsonField("creation_date")]
+ public virtual DateTime? CreationDate { get; set; }
+
+ ///
+ /// Gets or sets the unique identifier associated with the current authorization.
+ ///
+ [BsonId]
+ public virtual ObjectId Id { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the additional properties associated with the current authorization.
+ ///
+ [BsonField("properties")]
+ public virtual IReadOnlyDictionary? Properties { get; set; }
+
+ ///
+ /// Gets or sets the scopes associated with the current authorization.
+ ///
+ [BsonField("scopes")]
+ public virtual IReadOnlyList? Scopes { get; set; } = ImmutableList.Create();
+
+ ///
+ /// Gets or sets the status of the current authorization.
+ ///
+ [BsonField("status")]
+ public virtual string? Status { get; set; }
+
+ ///
+ /// Gets or sets the subject associated with the current authorization.
+ ///
+ [BsonField("subject")]
+ public virtual string? Subject { get; set; }
+
+ ///
+ /// Gets or sets the type of the current authorization.
+ ///
+ [BsonField("type")]
+ public virtual string? Type { get; set; }
+}
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs
new file mode 100644
index 0000000..8f76a80
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBScope.cs
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB.Models;
+
+///
+/// Represents an OpenIddict scope.
+///
+[DebuggerDisplay("Id = {Id.ToString(),nq} ; Name = {Name,nq}")]
+public class OpenIddictLiteDBScope
+{
+ ///
+ /// Gets or sets the concurrency token.
+ ///
+ [BsonField("concurrency_token")]
+ public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// Gets or sets the public description associated with the current scope.
+ ///
+ [BsonField("description")]
+ public virtual string? Description { get; set; }
+
+ ///
+ /// Gets or sets the localized public descriptions associated with the current scope.
+ ///
+ [BsonField("descriptions")]
+ public virtual IReadOnlyDictionary? Descriptions { get; set; }
+ = ImmutableDictionary.Create();
+
+ ///
+ /// Gets or sets the display name associated with the current scope.
+ ///
+ [BsonField("display_name")]
+ public virtual string? DisplayName { get; set; }
+
+ ///
+ /// Gets or sets the localized display names associated with the current scope.
+ ///
+ [BsonField("display_names")]
+ public virtual IReadOnlyDictionary? DisplayNames { get; set; }
+ = ImmutableDictionary.Create();
+
+ ///
+ /// Gets or sets the unique identifier associated with the current scope.
+ ///
+ [BsonId]
+ public virtual ObjectId Id { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the unique name associated with the current scope.
+ ///
+ [BsonField("name")]
+ public virtual string? Name { get; set; }
+
+ ///
+ /// Gets or sets the additional properties associated with the current scope.
+ ///
+ [BsonField("properties")]
+ public virtual IReadOnlyDictionary? Properties { get; set; }
+
+ ///
+ /// Gets or sets the resources associated with the current scope.
+ ///
+ [BsonField("resources")]
+ public virtual IReadOnlyList? Resources { get; set; } = ImmutableList.Create();
+}
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs
new file mode 100644
index 0000000..859b9e3
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/OpenIddictLiteDBToken.cs
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB.Models;
+
+///
+/// Represents an OpenIddict token.
+///
+[DebuggerDisplay("Id = {Id.ToString(),nq} ; Subject = {Subject,nq} ; Type = {Type,nq} ; Status = {Status,nq}")]
+public class OpenIddictLiteDBToken
+{
+ ///
+ /// Gets or sets the identifier of the application associated with the current token.
+ ///
+ [BsonField("application_id")]
+ public virtual ObjectId ApplicationId { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the identifier of the authorization associated with the current token.
+ ///
+ [BsonField("authorization_id")]
+ public virtual ObjectId AuthorizationId { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the concurrency token.
+ ///
+ [BsonField("concurrency_token")]
+ public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// Gets or sets the UTC creation date of the current token.
+ ///
+ [BsonField("creation_date")]
+ public virtual DateTime? CreationDate { get; set; }
+
+ ///
+ /// Gets or sets the UTC expiration date of the current token.
+ ///
+ [BsonField("expiration_date")]
+ public virtual DateTime? ExpirationDate { get; set; }
+
+ ///
+ /// Gets or sets the unique identifier associated with the current token.
+ ///
+ [BsonId]
+ public virtual ObjectId Id { get; set; } = ObjectId.Empty;
+
+ ///
+ /// Gets or sets the payload of the current token, if applicable.
+ /// Note: this property is only used for reference tokens
+ /// and may be encrypted for security reasons.
+ ///
+ [BsonField("payload")]
+ public virtual string? Payload { get; set; }
+
+ ///
+ /// Gets or sets the additional properties associated with the current token.
+ ///
+ [BsonField("properties")]
+ public virtual IReadOnlyDictionary? Properties { get; set; }
+
+ ///
+ /// Gets or sets the UTC redemption date of the current token.
+ ///
+ [BsonField("redemption_date")]
+ public virtual DateTime? RedemptionDate { get; set; }
+
+ ///
+ /// Gets or sets the reference identifier associated
+ /// with the current token, if applicable.
+ /// Note: this property is only used for reference tokens
+ /// and may be hashed or encrypted for security reasons.
+ ///
+ [BsonField("reference_id")]
+ public virtual string? ReferenceId { get; set; }
+
+ ///
+ /// Gets or sets the status of the current token.
+ ///
+ [BsonField("status")]
+ public virtual string? Status { get; set; }
+
+ ///
+ /// Gets or sets the subject associated with the current token.
+ ///
+ [BsonField("subject")]
+ public virtual string? Subject { get; set; }
+
+ ///
+ /// Gets or sets the type of the current token.
+ ///
+ [BsonField("type")]
+ public virtual string? Type { get; set; }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj b/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj
new file mode 100644
index 0000000..8c0f9ab
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/Sknet.OpenIddict.LiteDB.Models.csproj
@@ -0,0 +1,46 @@
+
+
+
+ net461;netstandard2.0;net6.0
+ enable
+ enable
+ 10.0
+
+ Steven Kuhn
+ Copyright (c) 2022 Steven Kuhn and contributors
+ Document-oriented entities for the OpenIddict LiteDB stores.
+ true
+ True
+ True
+ icon.png
+ MIT
+ https://github.com/stevenkuhn/openiddict-litedb
+ README.md
+ $(PackageTags);litedb;models
+ git
+ https://github.com/stevenkuhn/openiddict-litedb.git
+ snupkg
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs b/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs
new file mode 100644
index 0000000..954fe3d
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB.Models/Usings.cs
@@ -0,0 +1,4 @@
+global using LiteDB;
+global using System.Collections.Immutable;
+global using System.Diagnostics;
+global using System.Text.Json;
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs b/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs
new file mode 100644
index 0000000..68a872f
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/IOpenIddictLiteDBContext.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Exposes the LiteDB database used by the OpenIddict stores.
+///
+public interface IOpenIddictLiteDBContext
+{
+ ///
+ /// Gets the .
+ ///
+ ///
+ /// A that can be used to monitor the
+ /// asynchronous operation, whose result returns the LiteDB database.
+ ///
+ ValueTask GetDatabaseAsync(CancellationToken cancellationToken);
+}
diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs
new file mode 100644
index 0000000..14e08ca
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBBuilder.cs
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Exposes the necessary methods required to configure the OpenIddict LiteDB services.
+///
+public class OpenIddictLiteDBBuilder
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The services collection.
+ public OpenIddictLiteDBBuilder(IServiceCollection services)
+ => Services = services ?? throw new ArgumentNullException(nameof(services));
+
+ ///
+ /// Gets the services collection.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IServiceCollection Services { get; }
+
+ ///
+ /// Amends the default OpenIddict LiteDB configuration.
+ ///
+ /// The delegate used to configure the OpenIddict options.
+ /// This extension can be safely called multiple times.
+ /// The .
+ public OpenIddictLiteDBBuilder Configure(Action configuration)
+ {
+ if (configuration is null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ Services.Configure(configuration);
+
+ return this;
+ }
+
+ ///
+ /// Configures OpenIddict to use the specified entity as the default application entity.
+ ///
+ /// The .
+ public OpenIddictLiteDBBuilder ReplaceDefaultApplicationEntity()
+ where TApplication : OpenIddictLiteDBApplication
+ {
+ Services.Configure(options => options.DefaultApplicationType = typeof(TApplication));
+
+ return this;
+ }
+
+ ///
+ /// Configures OpenIddict to use the specified entity as the default authorization entity.
+ ///
+ /// The .
+ public OpenIddictLiteDBBuilder ReplaceDefaultAuthorizationEntity()
+ where TAuthorization : OpenIddictLiteDBAuthorization
+ {
+ Services.Configure(options => options.DefaultAuthorizationType = typeof(TAuthorization));
+
+ return this;
+ }
+
+ ///
+ /// Configures OpenIddict to use the specified entity as the default scope entity.
+ ///
+ /// The .
+ public OpenIddictLiteDBBuilder ReplaceDefaultScopeEntity()
+ where TScope : OpenIddictLiteDBScope
+ {
+ Services.Configure(options => options.DefaultScopeType = typeof(TScope));
+
+ return this;
+ }
+
+ ///
+ /// Configures OpenIddict to use the specified entity as the default token entity.
+ ///
+ /// The .
+ public OpenIddictLiteDBBuilder ReplaceDefaultTokenEntity()
+ where TToken : OpenIddictLiteDBToken
+ {
+ Services.Configure(options => options.DefaultTokenType = typeof(TToken));
+
+ return this;
+ }
+
+ ///
+ /// Replaces the default applications collection name (by default, openiddict.applications).
+ ///
+ /// The collection name
+ /// The .
+ public OpenIddictLiteDBBuilder SetApplicationsCollectionName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The collection name cannot be null or empty.", nameof(name));
+ }
+
+ return Configure(options => options.ApplicationsCollectionName = name);
+ }
+
+ ///
+ /// Replaces the default authorizations collection name (by default, openiddict.authorizations).
+ ///
+ /// The collection name
+ /// The .
+ public OpenIddictLiteDBBuilder SetAuthorizationsCollectionName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The collection name cannot be null or empty.", nameof(name));
+ }
+
+ return Configure(options => options.AuthorizationsCollectionName = name);
+ }
+
+ ///
+ /// Replaces the default scopes collection name (by default, openiddict.scopes).
+ ///
+ /// The collection name
+ /// The .
+ public OpenIddictLiteDBBuilder SetScopesCollectionName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The collection name cannot be null or empty.", nameof(name));
+ }
+
+ return Configure(options => options.ScopesCollectionName = name);
+ }
+
+ ///
+ /// Replaces the default tokens collection name (by default, openiddict.tokens).
+ ///
+ /// The collection name
+ /// The .
+ public OpenIddictLiteDBBuilder SetTokensCollectionName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The collection name cannot be null or empty.", nameof(name));
+ }
+
+ return Configure(options => options.TokensCollectionName = name);
+ }
+
+ ///
+ /// Configures the LiteDB stores to use the specified database
+ /// instead of retrieving it from the dependency injection container.
+ ///
+ /// The .
+ /// The .
+ public OpenIddictLiteDBBuilder UseDatabase(ILiteDatabase database)
+ {
+ if (database is null)
+ {
+ throw new ArgumentNullException(nameof(database));
+ }
+
+ return Configure(options => options.Database = database);
+ }
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override bool Equals(object? obj) => base.Equals(obj);
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override int GetHashCode() => base.GetHashCode();
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override string? ToString() => base.ToString();
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs
new file mode 100644
index 0000000..0f707bf
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBContext.cs
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+public class OpenIddictLiteDBContext : IOpenIddictLiteDBContext
+{
+ private readonly IOptionsMonitor _options;
+ private readonly IServiceProvider _provider;
+
+ public OpenIddictLiteDBContext(
+ IOptionsMonitor options,
+ IServiceProvider provider)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ ///
+ public ValueTask GetDatabaseAsync(CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return new(Task.FromCanceled(cancellationToken));
+ }
+
+ var database = _options.CurrentValue.Database;
+ if (database is null)
+ {
+ database = _provider.GetService();
+ }
+
+ if (database is null)
+ {
+ return new(Task.FromException(
+ new InvalidOperationException("No suitable LiteDB database service can be found.\r\nTo configure the OpenIddict LiteDB stores to use a specific database, use 'services.AddOpenIddict().AddCore().UseLiteDB().UseDatabase()' or register an 'ILiteDatabase' in the dependency injection container in 'ConfigureServices()'.")));
+ }
+
+ return new(database);
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs
new file mode 100644
index 0000000..05b6f5f
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBExtensions.cs
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Microsoft.Extensions.DependencyInjection;
+
+
+///
+/// Exposes extensions allowing to register the OpenIddict LiteDB services.
+///
+public static class OpenIddictLiteDBExtensions
+{
+ ///
+ /// Registers the LiteDB stores services in the DI container and
+ /// configures OpenIddict to use the LiteDB entities by default.
+ ///
+ /// The services builder used by OpenIddict to register new services.
+ /// This extension can be safely called multiple times.
+ /// The .
+ public static OpenIddictLiteDBBuilder UseLiteDB(this OpenIddictCoreBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ // Note: Mongo uses simple binary comparison checks by default so the additional
+ // query filtering applied by the default OpenIddict managers can be safely disabled.
+ // builder.DisableAdditionalFiltering();
+
+ builder.SetDefaultApplicationEntity()
+ .SetDefaultAuthorizationEntity()
+ .SetDefaultScopeEntity()
+ .SetDefaultTokenEntity();
+
+ // Note: the LiteDB stores/resolvers don't depend on scoped/transient services and thus
+ // can be safely registered as singleton services and shared/reused across requests.
+ builder.ReplaceApplicationStoreResolver(ServiceLifetime.Singleton)
+ .ReplaceAuthorizationStoreResolver(ServiceLifetime.Singleton)
+ .ReplaceScopeStoreResolver(ServiceLifetime.Singleton)
+ .ReplaceTokenStoreResolver(ServiceLifetime.Singleton);
+
+ builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBApplicationStore<>));
+ builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBAuthorizationStore<>));
+ builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBScopeStore<>));
+ builder.Services.TryAddSingleton(typeof(OpenIddictLiteDBTokenStore<>));
+
+ builder.Services.TryAddSingleton();
+
+ return new OpenIddictLiteDBBuilder(builder.Services);
+ }
+
+ ///
+ /// Registers the LiteDB stores services in the DI container and
+ /// configures OpenIddict to use the LiteDB entities by default.
+ ///
+ /// The services builder used by OpenIddict to register new services.
+ /// The configuration delegate used to configure the LiteDB services.
+ /// This extension can be safely called multiple times.
+ /// The .
+ public static OpenIddictCoreBuilder UseLiteDB(
+ this OpenIddictCoreBuilder builder, Action configuration)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configuration is null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ configuration(builder.UseLiteDB());
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs
new file mode 100644
index 0000000..1c2dbb3
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/OpenIddictLiteDBOptions.cs
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Provides various settings needed to configure the OpenIddict LiteDB integration.
+///
+public class OpenIddictLiteDBOptions
+{
+ ///
+ /// Gets or sets the name of the applications collection (by default, openiddict.applications).
+ ///
+ public string ApplicationsCollectionName { get; set; } = "openiddict.applications";
+
+ ///
+ /// Gets or sets the name of the authorizations collection (by default, openiddict.authorizations).
+ ///
+ public string AuthorizationsCollectionName { get; set; } = "openiddict.authorizations";
+
+ ///
+ /// Gets or sets the used by the OpenIddict stores.
+ /// If no value is explicitly set, the database is resolved from the DI container.
+ ///
+ public ILiteDatabase? Database { get; set; }
+
+ ///
+ /// Gets or sets the name of the scopes collection (by default, openiddict.scopes).
+ ///
+ public string ScopesCollectionName { get; set; } = "openiddict.scopes";
+
+ ///
+ /// Gets or sets the name of the tokens collection (by default, openiddict.tokens).
+ ///
+ public string TokensCollectionName { get; set; } = "openiddict.tokens";
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs
new file mode 100644
index 0000000..33712d3
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBApplicationStoreResolver.cs
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Exposes a method allowing to resolve an application store.
+///
+public class OpenIddictLiteDBApplicationStoreResolver : IOpenIddictApplicationStoreResolver
+{
+ private readonly ConcurrentDictionary _cache = new ConcurrentDictionary();
+ private readonly IServiceProvider _provider;
+
+ public OpenIddictLiteDBApplicationStoreResolver(IServiceProvider provider)
+ => _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+
+ ///
+ /// Returns an application store compatible with the specified application type or throws an
+ /// if no store can be built using the specified type.
+ ///
+ /// The type of the Application entity.
+ /// An .
+ public IOpenIddictApplicationStore Get() where TApplication : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TApplication), key =>
+ {
+ if (!typeof(OpenIddictLiteDBApplication).IsAssignableFrom(key))
+ {
+ throw new InvalidOperationException("The specified application type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBApplication' entity or a custom entity that inherits from the 'OpenIddictLiteDBApplication' entity.");
+ }
+
+ return typeof(OpenIddictLiteDBApplicationStore<>).MakeGenericType(key);
+ });
+
+ return (IOpenIddictApplicationStore)_provider.GetRequiredService(type);
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs
new file mode 100644
index 0000000..5f0938e
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBAuthorizationStoreResolver.cs
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Exposes a method allowing to resolve an authorization store.
+///
+public class OpenIddictLiteDBAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver
+{
+ private readonly ConcurrentDictionary _cache = new ConcurrentDictionary();
+ private readonly IServiceProvider _provider;
+
+ public OpenIddictLiteDBAuthorizationStoreResolver(IServiceProvider provider)
+ => _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+
+ ///
+ /// Returns an authorization store compatible with the specified authorization type or throws an
+ /// if no store can be built using the specified type.
+ ///
+ /// The type of the Authorization entity.
+ /// An .
+ public IOpenIddictAuthorizationStore Get() where TAuthorization : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TAuthorization), key =>
+ {
+ if (!typeof(OpenIddictLiteDBAuthorization).IsAssignableFrom(key))
+ {
+ throw new InvalidOperationException("The specified authorization type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBAuthorization' entity or a custom entity that inherits from the 'OpenIddictLiteDBAuthorization' entity.");
+ }
+
+ return typeof(OpenIddictLiteDBAuthorizationStore<>).MakeGenericType(key);
+ });
+
+ return (IOpenIddictAuthorizationStore)_provider.GetRequiredService(type);
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs
new file mode 100644
index 0000000..f97b749
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBScopeStoreResolver.cs
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Exposes a method allowing to resolve a scope store.
+///
+public class OpenIddictLiteDBScopeStoreResolver : IOpenIddictScopeStoreResolver
+{
+ private readonly ConcurrentDictionary _cache = new ConcurrentDictionary();
+ private readonly IServiceProvider _provider;
+
+ public OpenIddictLiteDBScopeStoreResolver(IServiceProvider provider)
+ => _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+
+ ///
+ /// Returns a scope store compatible with the specified scope type or throws an
+ /// if no store can be built using the specified type.
+ ///
+ /// The type of the Scope entity.
+ /// An .
+ public IOpenIddictScopeStore Get() where TScope : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TScope), key =>
+ {
+ if (!typeof(OpenIddictLiteDBScope).IsAssignableFrom(key))
+ {
+ throw new InvalidOperationException("The specified scope type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBScope' entity or a custom entity that inherits from the 'OpenIddictLiteDBScope' entity.");
+ }
+
+ return typeof(OpenIddictLiteDBScopeStore<>).MakeGenericType(key);
+ });
+
+ return (IOpenIddictScopeStore)_provider.GetRequiredService(type);
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs
new file mode 100644
index 0000000..dd7a791
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Resolvers/OpenIddictLiteDBTokenStoreResolver.cs
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Exposes a method allowing to resolve a token store.
+///
+public class OpenIddictLiteDBTokenStoreResolver : IOpenIddictTokenStoreResolver
+{
+ private readonly ConcurrentDictionary _cache = new ConcurrentDictionary();
+ private readonly IServiceProvider _provider;
+
+ public OpenIddictLiteDBTokenStoreResolver(IServiceProvider provider)
+ => _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+
+ ///
+ /// Returns a token store compatible with the specified token type or throws an
+ /// if no store can be built using the specified type.
+ ///
+ /// The type of the Token entity.
+ /// An .
+ public IOpenIddictTokenStore Get() where TToken : class
+ {
+ var store = _provider.GetService>();
+ if (store is not null)
+ {
+ return store;
+ }
+
+ var type = _cache.GetOrAdd(typeof(TToken), key =>
+ {
+ if (!typeof(OpenIddictLiteDBToken).IsAssignableFrom(key))
+ {
+ throw new InvalidOperationException("The specified token type is not compatible with the LiteDB stores.\r\nWhen enabling the LiteDB stores, make sure you use the built-in 'OpenIddictLiteDBToken' entity or a custom entity that inherits from the 'OpenIddictLiteDBToken' entity.");
+ }
+
+ return typeof(OpenIddictLiteDBTokenStore<>).MakeGenericType(key);
+ });
+
+ return (IOpenIddictTokenStore)_provider.GetRequiredService(type);
+ }
+}
\ No newline at end of file
diff --git a/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj b/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj
new file mode 100644
index 0000000..8f098f1
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Sknet.OpenIddict.LiteDB.csproj
@@ -0,0 +1,45 @@
+
+
+
+ net461;net472;net48;netstandard2.0;netstandard2.1;net6.0
+ enable
+ enable
+ 10.0
+
+ Steven Kuhn
+ Copyright (c) 2022 Steven Kuhn and contributors
+ LiteDB stores for OpenIddict.
+ true
+ True
+ True
+ icon.png
+ MIT
+ https://github.com/stevenkuhn/openiddict-litedb
+ README.md
+ $(PackageTags);litedb
+ git
+ https://github.com/stevenkuhn/openiddict-litedb.git
+ snupkg
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs
new file mode 100644
index 0000000..5c088d2
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBApplicationStore.cs
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Provides methods allowing to manage the applications stored in a database.
+///
+/// The type of the Application entity.
+public class OpenIddictLiteDBApplicationStore : IOpenIddictApplicationStore
+ where TApplication : OpenIddictLiteDBApplication
+{
+ public OpenIddictLiteDBApplicationStore(
+ IOpenIddictLiteDBContext context,
+ IOptionsMonitor options)
+ {
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ /// Gets the database context associated with the current store.
+ ///
+ protected IOpenIddictLiteDBContext Context { get; }
+
+ ///
+ /// Gets the options associated with the current store.
+ ///
+ protected IOptionsMonitor Options { get; }
+
+ ///
+ public virtual async ValueTask CountAsync(CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ return collection.LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CountAsync(
+ Func, IQueryable> query, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ return query(collection.FindAll().AsQueryable()).LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CreateAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ collection.Insert(application);
+ }
+
+ ///
+ public virtual async ValueTask DeleteAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ if (collection.DeleteMany(entity =>
+ entity.Id == application.Id &&
+ entity.ConcurrencyToken == application.ConcurrencyToken) == 0)
+ {
+ throw new OpenIddictExceptions.ConcurrencyException("The application was concurrently updated and cannot be persisted in its current state.\r\nReload the application from the database and retry the operation.");
+ }
+
+ // Delete the authorizations associated with the application.
+ database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName)
+ .DeleteMany(authorization => authorization.ApplicationId == application.Id);
+
+ // Delete the tokens associated with the application.
+ database.GetCollection(Options.CurrentValue.TokensCollectionName)
+ .DeleteMany(token => token.ApplicationId == application.Id);
+ }
+
+ ///
+ public virtual async ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ return collection.Find(application => application.ClientId == identifier).FirstOrDefault();
+ }
+
+ ///
+ public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ return collection.FindById(identifier);
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindByPostLogoutRedirectUriAsync(
+ string address, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(address))
+ {
+ throw new ArgumentException("The address cannot be null or empty.", nameof(address));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ var applications = collection.Query()
+ .Where(entity => entity.PostLogoutRedirectUris.Contains(address))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var application in applications)
+ {
+ yield return application;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindByRedirectUriAsync(
+ string address, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(address))
+ {
+ throw new ArgumentException("The address cannot be null or empty.", nameof(address));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ var applications = collection.Query()
+ .Where(entity => entity.RedirectUris.Contains(address))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var application in applications)
+ {
+ yield return application;
+ }
+ }
+ }
+
+ ///
+ public virtual async ValueTask GetAsync(
+ Func, TState, IQueryable> query,
+ TState state, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ return query(collection.FindAll().AsQueryable(), state).FirstOrDefault();
+ }
+
+ ///
+ public virtual ValueTask GetClientIdAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.ClientId);
+ }
+
+ ///
+ public virtual ValueTask GetClientSecretAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.ClientSecret);
+ }
+
+ ///
+ public virtual ValueTask GetClientTypeAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.Type);
+ }
+
+ ///
+ public virtual ValueTask GetConsentTypeAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.ConsentType);
+ }
+
+ ///
+ public virtual ValueTask GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.DisplayName);
+ }
+
+ ///
+ public virtual ValueTask> GetDisplayNamesAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.DisplayNames is not { Count: > 0 })
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(application.DisplayNames.ToImmutableDictionary(
+ pair => CultureInfo.GetCultureInfo(pair.Key),
+ pair => pair.Value));
+ }
+
+ ///
+ public virtual ValueTask GetIdAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return new(application.Id.ToString());
+ }
+
+ ///
+ public virtual ValueTask> GetPermissionsAsync(
+ TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.Permissions is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(application.Permissions.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask> GetPostLogoutRedirectUrisAsync(
+ TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.PostLogoutRedirectUris is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(application.PostLogoutRedirectUris.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask> GetPropertiesAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.Properties is null || application.Properties.Count == 0)
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(application.Properties.ToImmutableDictionary());
+ }
+
+ ///
+ public virtual ValueTask> GetRedirectUrisAsync(
+ TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.RedirectUris is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(application.RedirectUris.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask> GetRequirementsAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.Requirements is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(application.Requirements.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ return new(Activator.CreateInstance());
+ }
+
+ catch (MemberAccessException exception)
+ {
+ return new(Task.FromException(
+ new InvalidOperationException("An error occurred while trying to create a new application instance.\r\nMake sure that the application entity is not abstract and has a public parameterless constructor or create a custom application store that overrides 'InstantiateAsync()' to use a custom factory.", exception)));
+ }
+ }
+
+ ///
+ public virtual async IAsyncEnumerable ListAsync(
+ int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ var applications = collection
+ .Find(Query.All(), offset ?? 0, count ?? int.MaxValue)
+ .ToAsyncEnumerable();
+
+ await foreach (var application in applications)
+ {
+ yield return application;
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable ListAsync(
+ Func, TState, IQueryable> query,
+ TState state, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ var applications = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable();
+
+ await foreach (var application in applications)
+ {
+ yield return application;
+ }
+ }
+ }
+
+ ///
+ public virtual ValueTask SetClientIdAsync(TApplication application,
+ string? identifier, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ application.ClientId = identifier;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetClientSecretAsync(TApplication application,
+ string? secret, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ application.ClientSecret = secret;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetClientTypeAsync(TApplication application,
+ string? type, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ application.Type = type;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetConsentTypeAsync(TApplication application,
+ string? type, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ application.ConsentType = type;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetDisplayNameAsync(TApplication application,
+ string? name, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ application.DisplayName = name;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetDisplayNamesAsync(TApplication application,
+ ImmutableDictionary names, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (names is not { Count: > 0 })
+ {
+ application.DisplayNames = null;
+
+ return default;
+ }
+
+ application.DisplayNames = names.ToImmutableDictionary(
+ pair => pair.Key.Name,
+ pair => pair.Value);
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetPermissionsAsync(TApplication application, ImmutableArray permissions, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (permissions.IsDefaultOrEmpty)
+ {
+ application.Permissions = null;
+
+ return default;
+ }
+
+ application.Permissions = permissions.ToImmutableList();
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetPostLogoutRedirectUrisAsync(TApplication application,
+ ImmutableArray addresses, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (addresses.IsDefaultOrEmpty)
+ {
+ application.PostLogoutRedirectUris = null;
+
+ return default;
+ }
+
+ application.PostLogoutRedirectUris = addresses.ToImmutableList();
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetPropertiesAsync(TApplication application,
+ ImmutableDictionary properties, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (properties is not { Count: > 0 })
+ {
+ application.Properties = null;
+
+ return default;
+ }
+
+ application.Properties = properties;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetRedirectUrisAsync(TApplication application,
+ ImmutableArray addresses, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (addresses.IsDefaultOrEmpty)
+ {
+ application.RedirectUris = null;
+
+ return default;
+ }
+
+ application.RedirectUris = addresses.ToImmutableList();
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetRequirementsAsync(TApplication application,
+ ImmutableArray requirements, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (requirements.IsDefaultOrEmpty)
+ {
+ application.Requirements = null;
+
+ return default;
+ }
+
+ application.Requirements = requirements.ToImmutableList();
+
+ return default;
+ }
+
+ ///
+ public virtual async ValueTask UpdateAsync(TApplication application, CancellationToken cancellationToken)
+ {
+ if (application is null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ // Generate a new concurrency token and attach it
+ // to the application before persisting the changes.
+ var timestamp = application.ConcurrencyToken;
+ application.ConcurrencyToken = Guid.NewGuid().ToString();
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName);
+
+ if (collection.Count(entity => entity.Id == application.Id && entity.ConcurrencyToken == timestamp) == 0)
+ {
+ throw new ConcurrencyException("The application was concurrently updated and cannot be persisted in its current state.\r\nReload the application from the database and retry the operation.");
+ }
+
+ collection.Update(application);
+ }
+}
diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs
new file mode 100644
index 0000000..cee4548
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBAuthorizationStore.cs
@@ -0,0 +1,718 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Provides methods allowing to manage the authorizations stored in a database.
+///
+/// The type of the Authorization entity.
+public class OpenIddictLiteDBAuthorizationStore : IOpenIddictAuthorizationStore
+ where TAuthorization : OpenIddictLiteDBAuthorization
+{
+ public OpenIddictLiteDBAuthorizationStore(
+ IOpenIddictLiteDBContext context,
+ IOptionsMonitor options)
+ {
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ /// Gets the database context associated with the current store.
+ ///
+ protected IOpenIddictLiteDBContext Context { get; }
+
+ ///
+ /// Gets the options associated with the current store.
+ ///
+ protected IOptionsMonitor Options { get; }
+
+ ///
+ public virtual async ValueTask CountAsync(CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ return collection.LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CountAsync(
+ Func, IQueryable> query, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ return query(collection.FindAll().AsQueryable()).LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CreateAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ collection.Insert(authorization);
+ }
+
+ ///
+ public virtual async ValueTask DeleteAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ if (collection.DeleteMany(entity =>
+ entity.Id == authorization.Id &&
+ entity.ConcurrencyToken == authorization.ConcurrencyToken) == 0)
+ {
+ throw new OpenIddictExceptions.ConcurrencyException("The authorization was concurrently updated and cannot be persisted in its current state.\r\nReload the authorization from the database and retry the operation.");
+ }
+
+ // Delete the tokens associated with the authorization.
+ database.GetCollection(Options.CurrentValue.TokensCollectionName)
+ .DeleteMany(token => token.AuthorizationId == authorization.Id);
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindAsync(
+ string subject, string client, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity =>
+ entity.Subject == subject &&
+ entity.ApplicationId == new ObjectId(client))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindAsync(
+ string subject, string client,
+ string status, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity =>
+ entity.Subject == subject &&
+ entity.ApplicationId == new ObjectId(client) &&
+ entity.Status == status)
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindAsync(
+ string subject, string client,
+ string status, string type, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity =>
+ entity.Subject == subject &&
+ entity.ApplicationId == new ObjectId(client) &&
+ entity.Status == status &&
+ entity.Type == type)
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindAsync(
+ string subject, string client,
+ string status, string type,
+ ImmutableArray scopes, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity =>
+ entity.Subject == subject &&
+ entity.ApplicationId == new ObjectId(client) &&
+ entity.Status == status &&
+ entity.Type == type &&
+ Enumerable.All(scopes, scope => entity.Scopes.Contains(scope)))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindByApplicationIdAsync(
+ string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity => entity.ApplicationId == new ObjectId(identifier))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ return collection.FindById(identifier);
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindBySubjectAsync(
+ string subject, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection.Query()
+ .Where(entity => entity.Subject == subject)
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual ValueTask GetApplicationIdAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (authorization.ApplicationId == ObjectId.Empty)
+ {
+ return new(result: null);
+ }
+
+ return new(authorization.ApplicationId.ToString());
+ }
+
+ ///
+ public virtual async ValueTask GetAsync(
+ Func, TState, IQueryable> query,
+ TState state, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ return query(collection.FindAll().AsQueryable(), state).FirstOrDefault();
+ }
+
+ ///
+ public virtual ValueTask GetCreationDateAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (authorization.CreationDate is null)
+ {
+ return new(result: null);
+ }
+
+ return new(DateTime.SpecifyKind(authorization.CreationDate.Value, DateTimeKind.Utc));
+ }
+
+ ///
+ public virtual ValueTask GetIdAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ return new(authorization.Id.ToString());
+ }
+
+ ///
+ public virtual ValueTask> GetPropertiesAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (authorization.Properties is null || authorization.Properties.Count == 0)
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(authorization.Properties.ToImmutableDictionary());
+ }
+
+ ///
+ public virtual ValueTask> GetScopesAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (authorization.Scopes is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(authorization.Scopes.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask GetStatusAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ return new(authorization.Status);
+ }
+
+ ///
+ public virtual ValueTask GetSubjectAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ return new(authorization.Subject);
+ }
+
+ ///
+ public virtual ValueTask GetTypeAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ return new(authorization.Type);
+ }
+
+ ///
+ public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ return new(Activator.CreateInstance());
+ }
+
+ catch (MemberAccessException exception)
+ {
+ return new(Task.FromException(
+ new InvalidOperationException("An error occurred while trying to create a new authorization instance.\r\nMake sure that the authorization entity is not abstract and has a public parameterless constructor or create a custom authorization store that overrides 'InstantiateAsync()' to use a custom factory.", exception)));
+ }
+ }
+
+ ///
+ public virtual async IAsyncEnumerable ListAsync(
+ int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = collection
+ .Find(Query.All(), offset ?? 0, count ?? int.MaxValue)
+ .ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable ListAsync(
+ Func, TState, IQueryable> query,
+ TState state, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ var authorizations = query(collection.FindAll().AsQueryable(), state).ToAsyncEnumerable();
+
+ await foreach (var authorization in authorizations)
+ {
+ yield return authorization;
+ }
+ }
+ }
+
+ ///
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+
+ //var database = await Context.GetDatabaseAsync(cancellationToken);
+ //var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ // Note: directly deleting the resulting set of an aggregate query is not supported by MongoDb
+ // To work around this limitation, the authorization identifiers are stored in an intermediate
+ // list and delete requests are sent to remove the documents corresponding to these identifiers.
+
+ //var identifiers =
+ // await (from authorization in collection.AsQueryable()
+ // join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable()
+ // on authorization.Id equals token.AuthorizationId into tokens
+ // where authorization.CreationDate < threshold.UtcDateTime
+ // where authorization.Status != Statuses.Valid ||
+ // (authorization.Type == AuthorizationTypes.AdHoc && !tokens.Any())
+ // select authorization.Id).ToListAsync(cancellationToken);
+
+ // Note: to avoid generating delete requests with very large filters, a buffer is used here and the
+ // maximum number of elements that can be removed by a single call to PruneAsync() is deliberately limited.
+ //foreach (var buffer in Buffer(identifiers.Take(1_000_000), 1_000))
+ //{
+ // await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id), cancellationToken);
+ //}
+
+ //static IEnumerable> Buffer(IEnumerable source, int count)
+ //{
+ // List? buffer = null;
+
+ // foreach (var element in source)
+ // {
+ // buffer ??= new List(capacity: 1);
+ // buffer.Add(element);
+
+ // if (buffer.Count == count)
+ // {
+ // yield return buffer;
+
+ // buffer = null;
+ // }
+ // }
+
+ // if (buffer is not null)
+ // {
+ // yield return buffer;
+ // }
+ //}
+ }
+
+ ///
+ public virtual ValueTask SetApplicationIdAsync(TAuthorization authorization,
+ string? identifier, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (!string.IsNullOrEmpty(identifier))
+ {
+ authorization.ApplicationId = new ObjectId(identifier);
+ }
+
+ else
+ {
+ authorization.ApplicationId = ObjectId.Empty;
+ }
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetCreationDateAsync(TAuthorization authorization,
+ DateTimeOffset? date, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ authorization.CreationDate = date?.UtcDateTime;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetPropertiesAsync(TAuthorization authorization,
+ ImmutableDictionary properties, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (properties is not { Count: > 0 })
+ {
+ authorization.Properties = null;
+
+ return default;
+ }
+
+ authorization.Properties = properties;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetScopesAsync(TAuthorization authorization,
+ ImmutableArray scopes, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ if (scopes.IsDefaultOrEmpty)
+ {
+ authorization.Scopes = null;
+
+ return default;
+ }
+
+ authorization.Scopes = scopes.ToImmutableList();
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetStatusAsync(TAuthorization authorization, string? status, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ authorization.Status = status;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetSubjectAsync(TAuthorization authorization, string? subject, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ authorization.Subject = subject;
+
+ return default;
+ }
+
+ ///
+ public virtual ValueTask SetTypeAsync(TAuthorization authorization, string? type, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ authorization.Type = type;
+
+ return default;
+ }
+
+ ///
+ public virtual async ValueTask UpdateAsync(TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization is null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ // Generate a new concurrency token and attach it
+ // to the authorization before persisting the changes.
+ var timestamp = authorization.ConcurrencyToken;
+ authorization.ConcurrencyToken = Guid.NewGuid().ToString();
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+
+ if (collection.Count(entity => entity.Id == authorization.Id && entity.ConcurrencyToken == timestamp) == 0)
+ {
+ throw new ConcurrencyException("The authorization was concurrently updated and cannot be persisted in its current state.\r\nReload the authorization from the database and retry the operation.");
+ }
+
+ collection.Update(authorization);
+ }
+}
+
diff --git a/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs
new file mode 100644
index 0000000..c86cd81
--- /dev/null
+++ b/src/Sknet.OpenIddict.LiteDB/Stores/OpenIddictLiteDBScopeStore.cs
@@ -0,0 +1,517 @@
+/*
+ * Copyright (c) 2022 Steven Kuhn and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+namespace Sknet.OpenIddict.LiteDB;
+
+///
+/// Provides methods allowing to manage the scopes stored in a database.
+///
+/// The type of the Scope entity.
+public class OpenIddictLiteDBScopeStore : IOpenIddictScopeStore
+ where TScope : OpenIddictLiteDBScope
+{
+ public OpenIddictLiteDBScopeStore(
+ IOpenIddictLiteDBContext context,
+ IOptionsMonitor options)
+ {
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ /// Gets the database context associated with the current store.
+ ///
+ protected IOpenIddictLiteDBContext Context { get; }
+
+ ///
+ /// Gets the options associated with the current store.
+ ///
+ protected IOptionsMonitor Options { get; }
+
+ ///
+ public virtual async ValueTask CountAsync(CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ return collection.LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CountAsync(
+ Func, IQueryable> query, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ return query(collection.FindAll().AsQueryable()).LongCount();
+ }
+
+ ///
+ public virtual async ValueTask CreateAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ collection.Insert(scope);
+ }
+
+ ///
+ public virtual async ValueTask DeleteAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ if (collection.DeleteMany(entity =>
+ entity.Id == scope.Id &&
+ entity.ConcurrencyToken == scope.ConcurrencyToken) == 0)
+ {
+ throw new OpenIddictExceptions.ConcurrencyException("The scope was concurrently updated and cannot be persisted in its current state.\r\nReload the scope from the database and retry the operation.");
+ }
+ }
+
+ ///
+ public virtual async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ return collection.FindById(identifier);
+ }
+
+ ///
+ public virtual async ValueTask FindByNameAsync(string name, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ return collection.Query()
+ .Where(entity => entity.Name == name)
+ .FirstOrDefault();
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken)
+ {
+ if (names.Any(name => string.IsNullOrEmpty(name)))
+ {
+ throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ var scopes = collection.Query()
+ .Where(entity => Enumerable.Contains(names, entity.Name))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var scope in scopes)
+ {
+ yield return scope;
+ }
+ }
+ }
+
+ ///
+ public virtual IAsyncEnumerable FindByResourceAsync(string resource, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(resource))
+ {
+ throw new ArgumentException("The resource cannot be null or empty.", nameof(resource));
+ }
+
+ return ExecuteAsync(cancellationToken);
+
+ async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ var scopes = collection.Query()
+ .Where(entity => entity.Resources.Contains(resource))
+ .ToEnumerable().ToAsyncEnumerable();
+
+ await foreach (var scope in scopes)
+ {
+ yield return scope;
+ }
+ }
+ }
+
+ ///
+ public virtual async ValueTask GetAsync(
+ Func, TState, IQueryable> query,
+ TState state, CancellationToken cancellationToken)
+ {
+ if (query is null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName);
+
+ return query(collection.FindAll().AsQueryable(), state).FirstOrDefault();
+ }
+
+ ///
+ public virtual ValueTask GetDescriptionAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ return new(scope.Description);
+ }
+
+ ///
+ public virtual ValueTask> GetDescriptionsAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ if (scope.Descriptions is not { Count: > 0 })
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(scope.Descriptions.ToImmutableDictionary(
+ pair => CultureInfo.GetCultureInfo(pair.Key),
+ pair => pair.Value));
+ }
+
+ ///
+ public virtual ValueTask GetDisplayNameAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ return new(scope.DisplayName);
+ }
+
+ ///
+ public virtual ValueTask> GetDisplayNamesAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ if (scope.DisplayNames is not { Count: > 0 })
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(scope.DisplayNames.ToImmutableDictionary(
+ pair => CultureInfo.GetCultureInfo(pair.Key),
+ pair => pair.Value));
+ }
+
+ ///
+ public virtual ValueTask GetIdAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ return new(scope.Id.ToString());
+ }
+
+ ///
+ public virtual ValueTask GetNameAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ return new(scope.Name);
+ }
+
+ ///
+ public virtual ValueTask> GetPropertiesAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ if (scope.Properties is null || scope.Properties.Count == 0)
+ {
+ return new(ImmutableDictionary.Create());
+ }
+
+ return new(scope.Properties.ToImmutableDictionary());
+ }
+
+ ///
+ public virtual ValueTask> GetResourcesAsync(TScope scope, CancellationToken cancellationToken)
+ {
+ if (scope is null)
+ {
+ throw new ArgumentNullException(nameof(scope));
+ }
+
+ if (scope.Resources is not { Count: > 0 })
+ {
+ return new(ImmutableArray.Create());
+ }
+
+ return new(scope.Resources.ToImmutableArray());
+ }
+
+ ///
+ public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ return new(Activator.CreateInstance());
+ }
+
+ catch (MemberAccessException exception)
+ {
+ return new(Task.FromException