Merge branch 'namc/mondo' into release/2.1

This commit is contained in:
Nate McMaster 2018-11-09 14:45:29 -08:00
commit 4db2631735
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
176 changed files with 11316 additions and 496 deletions

View File

@ -3,28 +3,20 @@ trigger:
- release/*
jobs:
- template: project-ci.yml
- template: jobs/default-build.yml
parameters:
jobName: PR_FastCheck
jobDisplayName: Fast Check
agentOs: Windows
buildArgs: "/t:FastCheck"
- job: RepoBuilds
pool:
vmImage: vs2017-win2016
strategy:
maxParallel: 3
matrix:
DataProtection:
_FolderName: DataProtection
WebSockets:
_FolderName: WebSockets
steps:
- script: src/$(_FolderName)/build.cmd -ci
displayName: Run src/$(_FolderName)/build.cmd
- task: PublishTestResults@2
displayName: Publish test results
condition: always()
inputs:
testRunner: vstest
testResultsFiles: 'src/$(_FolderName)/artifacts/logs/**/*.trx'
- template: jobs/default-build.yml
parameters:
jobName: Windows_Build
jobDisplayName: "Build: Windows"
agentOs: Windows
beforeBuild:
- powershell: "& ./src/IISIntegration/tools/UpdateIISExpressCertificate.ps1"
displayName: Setup IISExpress test certificates"
- template: jobs/iisintegration-job.yml
parameters:
variables:

View File

@ -63,6 +63,7 @@ jobs:
- job: ${{ coalesce(parameters.jobName, parameters.agentOs) }}
displayName: ${{ coalesce(parameters.jobDisplayName, parameters.agentOs) }}
dependsOn: ${{ parameters.dependsOn }}
timeoutInMinutes: 90
workspace:
clean: all
strategy:

4
.gitmodules vendored
View File

@ -66,10 +66,6 @@
path = modules/JavaScriptServices
url = https://github.com/aspnet/JavaScriptServices.git
branch = release/2.1
[submodule "modules/JsonPatch"]
path = modules/JsonPatch
url = https://github.com/aspnet/JsonPatch.git
branch = release/2.1
[submodule "modules/KestrelHttpServer"]
path = modules/KestrelHttpServer
url = https://github.com/aspnet/KestrelHttpServer.git

View File

@ -34,7 +34,15 @@
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
</PropertyGroup>
<PropertyGroup>
<StandardTestTfms>netcoreapp2.1;net461</StandardTestTfms>
</PropertyGroup>
<Import Project="eng\Dependencies.props" />
<Import Project="eng\PatchConfig.props" />
<Import Project="eng\ProjectReferences.props" />
<Import Project="eng\targets\Wix.Common.props" Condition="'$(MSBuildProjectExtension)' == '.wixproj'" />
<Import Project="eng\targets\CSharp.Common.props" Condition="'$(MSBuildProjectExtension)' == '.csproj'" />
<Import Project="eng\targets\Cpp.Common.props" Condition="'$(MSBuildProjectExtension)' == '.vcxproj'" />
</Project>

View File

@ -1,9 +1,39 @@
<Project>
<!-- Properties which should be set after the project has been evaluated -->
<PropertyGroup Condition=" '$(MSBuildProjectExtension)' == '.csproj' ">
<PackageId Condition=" '$(PackageId)' == '' ">$(AssemblyName)</PackageId>
<IsPackable Condition="'$(IsPackable)' == '' AND ( '$(IsTestProject)' == 'true' OR '$(IsTestAssetProject)' == 'true' OR '$(IsBenchmarkProject)' == 'true' OR '$(IsSampleProject)' == 'true' ) ">false</IsPackable>
</PropertyGroup>
<Import Project="eng\Baseline.props" />
<PropertyGroup Condition=" '$(IsPackable)' != 'false' AND '$(AspNetCorePatchVersion)' != '0' ">
<!-- Always include framework metapackages in patch updates. -->
<IsPackageInThisPatch Condition="'$(IsFrameworkMetapackage)' == 'true'">true</IsPackageInThisPatch>
<IsPackageInThisPatch Condition="'$(IsPackageInThisPatch)' == ''">$(PackagesInPatch.Contains(' $(PackageId);'))</IsPackageInThisPatch>
</PropertyGroup>
<PropertyGroup Condition=" '$(IsPackable)' != 'false' AND '$(IsServicingBuild)' == 'true' ">
<!-- Used to distinguish between packages building -->
<IsPackableInNonServicingBuild>true</IsPackableInNonServicingBuild>
<!-- Suppress creation of .nupkg for servicing builds. -->
<IsPackable Condition=" '$(IsPackageInThisPatch)' != 'true' ">false</IsPackable>
</PropertyGroup>
<PropertyGroup Condition=" '$(IsPackageInThisPatch)' != 'true' AND '$(BaselinePackageVersion)' != '' ">
<!-- This keeps assembly and package versions consistent across patches. If a package is not included in a patch, its version should stay at the baseline. -->
<Version>$(BaselinePackageVersion).0</Version>
<AssemblyVersion>$(BaselinePackageVersion).0</AssemblyVersion>
<PackageVersion>$(BaselinePackageVersion)</PackageVersion>
</PropertyGroup>
<PropertyGroup>
<!-- Implementation projects are the projects which produce nuget packages or shipping assemblies. -->
<IsImplementationProject Condition=" '$(IsImplementationProject)' == '' AND '$(IsTestAssetProject)' != 'true' AND '$(IsTestProject)' != 'true' AND '$(IsBenchmarkProject)' != 'true' AND '$(IsSampleProject)' != 'true' ">true</IsImplementationProject>
<!-- Suppress KoreBuild warnings about the mismatch of repo version and local project version. The versioning in this mega repo is sufficiently complicated that KoreBuild's validation isn't helpful. -->
<VerifyVersion>false</VerifyVersion>
<EnableApiCheck Condition=" '$(EnableApiCheck)' != '' ">$(IsImplementationProject)</EnableApiCheck>
<IsPackable Condition="'$(IsPackable)' == '' AND '$(IsImplementationProject)' == 'true' ">true</IsPackable>
<IsPackable Condition="'$(IsPackable)' == '' ">false</IsPackable>
@ -14,5 +44,6 @@
</PropertyGroup>
<Import Project="eng\targets\Wix.Common.targets" Condition="'$(MSBuildProjectExtension)' == '.wixproj'" />
<Import Project="eng\targets\CSharp.Common.targets" Condition="'$(MSBuildProjectExtension)' == '.csproj'" />
<Import Project="eng\targets\Cpp.Common.targets" Condition="'$(MSBuildProjectExtension)' == '.vcxproj'" />
</Project>

View File

@ -33,7 +33,7 @@
</ItemGroup>
</Target>
<Target Name="_BuildMetapackage" DependsOnTargets="ResolveRepoInfo">
<Target Name="_BuildMetapackage" DependsOnTargets="GetProjectArtifactInfo;ResolveRepoInfo">
<PropertyGroup>
<MetapackageSource>$(_MetapackageSrcRoot)$(MetapackageName)\</MetapackageSource>
<MetapackageWorkDirectory>$(_WorkRoot)pkg\$(MetapackageName)\</MetapackageWorkDirectory>

View File

@ -7,7 +7,6 @@
</ItemDefinitionGroup>
<ItemGroup>
<RepositoryBuildOrder Include="JsonPatch" Order="1" />
<RepositoryBuildOrder Include="DotNetTools" Order="1" />
<RepositoryBuildOrder Include="HtmlAbstractions" Order="1" />
<RepositoryBuildOrder Include="Razor" Order="6" />
@ -30,7 +29,6 @@
<RepositoryBuildOrder Include="ServerTests" Order="11" />
<RepositoryBuildOrder Include="Diagnostics" Order="12" />
<RepositoryBuildOrder Include="Localization" Order="12" />
<RepositoryBuildOrder Include="WebSockets" Order="13" RootPath="$(RepositoryRoot)src\WebSockets\" />
<RepositoryBuildOrder Include="Security" Order="13" />
<RepositoryBuildOrder Include="MetaPackages" Order="13" />
<RepositoryBuildOrder Include="Mvc" Order="14" />

View File

@ -15,6 +15,7 @@
<!-- Determined by build tools -->
<InternalAspNetCoreSdkPackageVersion>$(KoreBuildVersion)</InternalAspNetCoreSdkPackageVersion>
<InternalAspNetCoreSiteExtensionSdkPackageVersion>$(KoreBuildVersion)</InternalAspNetCoreSiteExtensionSdkPackageVersion>
<InternalAspNetCoreSdkPackageVersion Condition=" '$(KoreBuildVersion)' == '' ">2.1.3-rtm-15842</InternalAspNetCoreSdkPackageVersion>
</PropertyGroup>
<!-- These are package versions that should not be overridden or updated by automation. -->

View File

@ -2,9 +2,8 @@
<PropertyGroup>
<!-- This repo does not have solutions to build -->
<DisableDefaultTargets>true</DisableDefaultTargets>
<GenerateSignRequest>false</GenerateSignRequest>
<SignType Condition=" '$(SignType)' == '' ">public</SignType>
<DisableDefaultItems>true</DisableDefaultItems>
<BuildSolutions>false</BuildSolutions>
<SkipTests>false</SkipTests>
<SkipTests Condition="'$(CompileOnly)' == 'true'">true</SkipTests>
@ -42,6 +41,16 @@
<SharedFrameworkName Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectToExclude Include="$(RepositoryRoot)src\Middleware\WebSockets\samples\**\*.csproj" />
<ProjectToBuild Include="
$(RepositoryRoot)src\Features\JsonPatch\**\*.*proj;
$(RepositoryRoot)src\Middleware\**\*.*proj;
"
Exclude="@(ProjectToExclude)" />
</ItemGroup>
<!-- Properties for publishing -->
<PropertyGroup>
<!-- myget = non-orchestrated builds -->

View File

@ -16,18 +16,52 @@
<PrepareDependsOn>SetTeamCityBuildNumberToVersion;$(PrepareDependsOn);VerifyPackageArtifactConfig;VerifyExternalDependencyConfig;PrepareOutputPaths</PrepareDependsOn>
<CleanDependsOn>$(CleanDependsOn);CleanArtifacts;CleanRepoArtifacts</CleanDependsOn>
<RestoreDependsOn>$(RestoreDependsOn);InstallDotNet</RestoreDependsOn>
<CompileDependsOn>$(CompileDependsOn);BuildRepositories</CompileDependsOn>
<RestoreDependsOn>$(RestoreDependsOn);InstallDotNet;RestoreProjects</RestoreDependsOn>
<CompileDependsOn>$(CompileDependsOn);BuildProjects;PackProjects;BuildRepositories</CompileDependsOn>
<PackageDependsOn Condition="'$(TestOnly)' != 'true'">$(PackageDependsOn);BuildMetapackages;CheckExpectedPackagesExist</PackageDependsOn>
<TestDependsOn>$(TestDependsOn);_TestRepositories</TestDependsOn>
<GetArtifactInfoDependsOn>$(GetArtifactInfoDependsOn);ResolveRepoInfo</GetArtifactInfoDependsOn>
<TestDependsOn>$(TestDependsOn);TestProjects;_TestRepositories</TestDependsOn>
<GetArtifactInfoDependsOn>$(GetArtifactInfoDependsOn);GetProjectArtifactInfo;ResolveRepoInfo</GetArtifactInfoDependsOn>
</PropertyGroup>
<Target Name="PrepareOutputPaths">
<MakeDir Directories="$(ArtifactsDir);$(BuildDir)" />
</Target>
<Target Name="ResolveRepoInfo" DependsOnTargets="_PrepareRepositories;GetMetapackageArtifactInfo;GetLineupPackageInfo">
<Target Name="GenerateProjectList" DependsOnTargets="ResolveProjects">
<MSBuild Projects="@(ProjectToBuild)"
Targets="GetReferencesProvided"
BuildInParallel="true"
SkipNonexistentTargets="true"
SkipNonexistentProjects="true" >
<Output TaskParameter="TargetOutputs" ItemName="_ProjectReferenceProvider"/>
</MSBuild>
<PropertyGroup>
<ProjectListFile>$(MSBuildThisFileDirectory)..\eng\ProjectReferences.props</ProjectListFile>
<ProjectListContent>
<![CDATA[
<!-- This file is automatically generated. Run `build.cmd /t:GenerateProjectList` to update. -->
<Project>
<ItemGroup>
@(_ProjectReferenceProvider->'<ProjectReferenceProvider Include="%(Identity)" ProjectPath="%24(RepositoryRoot)%(ProjectFileRelativePath)" />', '%0A ')
</ItemGroup>
</Project>
]]>
</ProjectListContent>
</PropertyGroup>
<WriteLinesToFile File="$(ProjectListFile)" Lines="$(ProjectListContent)" Overwrite="true" />
</Target>
<Target Name="_ResolveProjectArtifactsInfoShipped" AfterTargets="GetProjectArtifactInfo">
<ItemGroup>
<ShippedArtifactInfo Include="@(ArtifactInfo)" Condition="'%(ArtifactInfo.IsShipped)' == 'true'" />
<ArtifactInfo Remove="@(ShippedArtifactInfo)" />
</ItemGroup>
</Target>
<Target Name="ResolveRepoInfo" DependsOnTargets="_ResolveProjectArtifactsInfoShipped;_PrepareRepositories;GetMetapackageArtifactInfo;GetLineupPackageInfo">
<!-- We need to pass the NETCoreApp package versions to msbuild so that it doesn't complain about us using a different one than it was restored against. -->
<PropertyGroup>
<DesignTimeBuildProps>MicrosoftNETCoreAppPackageVersion=$(MicrosoftNETCoreAppPackageVersion);</DesignTimeBuildProps>
@ -216,7 +250,7 @@
<WriteLinesToFile File="$(RepositoryRoot)artifacts\packages.csv" Lines="PackageId,Version;@(ArtifactInfo->WithMetadataValue('ArtifactType', 'NuGetPackage')->'%(PackageId),%(Version)')" Overwrite="true" />
</Target>
<Target Name="ComputeGraph" DependsOnTargets="ResolveRepoInfo;GeneratePropsFiles">
<Target Name="ComputeGraph" DependsOnTargets="GetProjectArtifactInfo;ResolveRepoInfo;GeneratePropsFiles">
<RepoTasks.CheckRepoGraph Condition=" ! $([MSBuild]::IsOSUnixLike())"
Solutions="@(Solution)"
Artifacts="@(ArtifactInfo);@(ShippedArtifactInfo)"
@ -272,8 +306,14 @@
Condition=" @(ExternalDependency->WithMetadataValue('Version', '')->Count()) != 0 " />
</Target>
<Target Name="_SetDesignTimeBuild">
<PropertyGroup>
<BuildProperties>$(BuildProperties);DesignTimeBuild=true</BuildProperties>
</PropertyGroup>
</Target>
<Target Name="FastCheck"
DependsOnTargets="ComputeGraph;VerifyPackageArtifactConfig;VerifyAllReposHaveNuGetPackageVerifier" />
DependsOnTargets="_SetDesignTimeBuild;ComputeGraph;VerifyPackageArtifactConfig;VerifyAllReposHaveNuGetPackageVerifier" />
<Target Name="CheckExpectedPackagesExist">
<ItemGroup>

View File

@ -77,6 +77,5 @@
<ShippedRepository Include="Session" />
<ShippedRepository Include="SignalR" />
<ShippedRepository Include="StaticFiles" />
<ShippedRepository Include="WebSockets" RootPath="$(RepositoryRoot)src\WebSockets\" />
</ItemGroup>
</Project>

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Remove="Internal.AspNetCore.Sdk" />
<PackageReference Include="Microsoft.DotNet.Archive" Version="$(MicrosoftDotNetArchivePackageVersion)" />
<PackageReference Include="NuGet.Build.Tasks" Version="$(NuGetInMSBuildVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(DevDependency_MicrosoftExtensionsDependencyModelPackageVersion)" PrivateAssets="All" />

View File

@ -10,11 +10,15 @@ In order to prepare this repo to build a new servicing update, the following cha
+ <AspNetCorePatchVersion>8</AspNetCorePatchVersion>
```
* Update the package archive baselines. This is used to make sure each build
of the package archives we give to Azure only contains new files and does
* Update the package archive baselines. This is used to make sure each build of the package archives we give to Azure only contains new files and does
not require overwriting existing files. See [src/PackageArchive/ZipManifestGenerator/](/src/PackageArchive/ZipManifestGenerator/README.md) for instructions on how to run this tool.
* Update the list of repositories which will contain changes in [build/submodules.props](/build/submodules.props).
* Update the package baselines. This is used to ensure packages keep a consistent set of dependencies between releases.
See [eng/tools/BaselineGenerator/](/eng/tools/BaselineGenerator/README.md) for instructions on how to run this tool.
* **For packages with source code in this repo (not a submodule):** Update the list of packages in [eng/PatchConfig.props](/eng/PatchConfig.props) to list which packages should be patching in this release.
* **For packages still building from submodules:** Update the list of repositories which will contain changes in [build/submodules.props](/build/submodules.props).
* `<ShippedRepository>` items represent repos which were released in a previous patch, and will not contain servicing updates in the next patch.
* `<Repository>` items represent repos which will produce new packages in this patch.

100
eng/Baseline.props Normal file
View File

@ -0,0 +1,100 @@
<!-- Auto generated. Do not edit manually, use eng/tools/BaselineGenerator/ to recreate. -->
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<AspNetCoreBaselineVersion>2.1.6</AspNetCoreBaselineVersion>
</PropertyGroup>
<!-- Package: Microsoft.AspNetCore.Cryptography.Internal-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Cryptography.Internal' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Cryptography.Internal' AND '$(TargetFramework)' == 'netstandard2.0' " />
<!-- Package: Microsoft.AspNetCore.Cryptography.KeyDerivation-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Cryptography.KeyDerivation' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Cryptography.KeyDerivation' AND '$(TargetFramework)' == 'netcoreapp2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Cryptography.Internal" Version="[2.1.1, )" />
</ItemGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Cryptography.KeyDerivation' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Cryptography.Internal" Version="[2.1.1, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Cryptography.Internal" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Win32.Registry" Version="[4.5.0, )" />
<BaselinePackageReference Include="System.Security.Cryptography.Xml" Version="[4.5.0, )" />
<BaselinePackageReference Include="System.Security.Principal.Windows" Version="[4.5.0, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection.Abstractions-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Abstractions' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' " />
<!-- Package: Microsoft.AspNetCore.DataProtection.AzureKeyVault-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.AzureKeyVault' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.AzureKeyVault' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Azure.KeyVault" Version="[2.3.2, )" />
<BaselinePackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="[3.14.2, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection.AzureStorage-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.AzureStorage' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.AzureStorage' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[2.1.1, )" />
<BaselinePackageReference Include="WindowsAzure.Storage" Version="[8.1.4, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection.Extensions-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Extensions' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Extensions' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.DependencyInjection" Version="[2.1.1, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection.Redis-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Redis' ">
<BaselinePackageVersion>0.4.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.Redis' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[2.1.1, )" />
<BaselinePackageReference Include="StackExchange.Redis.StrongName" Version="[1.2.4, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.DataProtection.SystemWeb-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.SystemWeb' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.DataProtection.SystemWeb' AND '$(TargetFramework)' == 'net461' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.DependencyInjection" Version="[2.1.1, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.JsonPatch-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.JsonPatch' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.JsonPatch' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.CSharp" Version="[4.5.0, )" />
<BaselinePackageReference Include="Newtonsoft.Json" Version="[11.0.2, )" />
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.WebSockets-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.WebSockets' ">
<BaselinePackageVersion>2.1.1</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.WebSockets' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="[2.1.1, )" />
<BaselinePackageReference Include="Microsoft.Extensions.Options" Version="[2.1.1, )" />
<BaselinePackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="[4.5.1, )" />
</ItemGroup>
</Project>

46
eng/Dependencies.props Normal file
View File

@ -0,0 +1,46 @@
<!-- This file is a work in progress as we merge repos and move content here from build/dependencies.props. -->
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ItemGroup Label="ProdCon dependencies">
<!-- These dependencies must use version variables because they may be overriden by ProdCon builds. -->
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
<LatestPackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
</ItemGroup>
<ItemGroup Label="External dependencies">
<LatestPackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<LatestPackageReference Include="Moq" Version="4.10.0" />
<!-- This version is required by MSBuild tasks or Visual Studio extensions. -->
<LatestPackageReference Include="Newtonsoft.Json" Version="9.0.1" Condition="'$(UseMSBuildJsonNet)' == 'true'" />
<!-- This version should be used by runtime packages -->
<LatestPackageReference Include="Newtonsoft.Json" Version="11.0.2" Condition="'$(UseMSBuildJsonNet)' != 'true'" />
<LatestPackageReference Include="xunit.abstractions" Version="2.0.1" />
<LatestPackageReference Include="xunit.analyzers" Version="0.10.0" />
<LatestPackageReference Include="xunit.assert" Version="2.3.1" />
<LatestPackageReference Include="xunit.extensibility.core" Version="2.3.1" />
<LatestPackageReference Include="xunit.extensibility.execution" Version="2.3.1" />
<LatestPackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<LatestPackageReference Include="xunit" Version="2.4.0" />
</ItemGroup>
<ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.NET.Test.Sdk" />
<Reference Include="Moq" />
<Reference Include="xunit" />
<Reference Include="xunit.analyzers" />
<Reference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>

13
eng/PatchConfig.props Normal file
View File

@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.7' ">
<PackagesInPatch>
Microsoft.AspNetCore;
Microsoft.AspNetCore.Server.IISIntegration;
</PackagesInPatch>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,7 @@
<!-- This file is automatically generated. Run `build.cmd /t:GenerateProjectList` to update. -->
<Project>
<ItemGroup>
<ProjectReferenceProvider Include="Microsoft.AspNetCore.JsonPatch" ProjectPath="$(RepositoryRoot)src\Features\JsonPatch\src\Microsoft.AspNetCore.JsonPatch.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebSockets" ProjectPath="$(RepositoryRoot)src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<LangVersion>7.2</LangVersion>
<!-- Instructs the compiler to use SHA256 instead of SHA1 when adding file hashes to PDBs. -->
<ChecksumAlgorithm>SHA256</ChecksumAlgorithm>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,5 @@
<Project>
<Import Project="Packaging.targets" />
<Import Project="ResolveReferences.targets" />
</Project>

View File

@ -0,0 +1,30 @@
<Project>
<Target Name="EnsureBaselineIsUpdated"
Condition="'$(IsServicingBuild)' == 'true' AND '$(AspNetCoreBaselineVersion)' != '$(PreviousAspNetCoreReleaseVersion)'"
BeforeTargets="BeforeBuild">
<Error Text="The package baseline ($(AspNetCoreBaselineVersion)) is out of date with the latest release of this repo ($(PreviousAspNetCoreReleaseVersion)).
See $(RepositoryRoot)eng\tools\BaselineGenerator\README.md for instructions on updating this baseline." />
</Target>
<!-- Temporary: this target is used to gather version information to pass to submodule builds. This can be removed after we finish merging submodules. -->
<Target Name="GetBaselineArtifactInfo"
Condition="'$(IsPackableInNonServicingBuild)' == 'true'"
Returns="@(ArtifactInfo)"
BeforeTargets="GetArtifactInfo">
<PropertyGroup>
<FullPackageOutputPath>$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg</FullPackageOutputPath>
</PropertyGroup>
<ItemGroup>
<ArtifactInfo Include="$(FullPackageOutputPath)">
<ArtifactType>NuGetPackage</ArtifactType>
<PackageId>$(PackageId)</PackageId>
<Version>$(PackageVersion)</Version>
<RepositoryRoot>$(RepositoryRoot)</RepositoryRoot>
<IsShipped>true</IsShipped>
</ArtifactInfo>
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,135 @@
<Project>
<PropertyGroup>
<ResolveReferencesDependsOn>
ResolveCustomReferences;
$(ResolveReferencesDependsOn);
</ResolveReferencesDependsOn>
</PropertyGroup>
<PropertyGroup>
<!--
Projects should only use the latest package references when:
* preparing a new major or minor release (i.e. a non-servicing builds)
* when a project is a test or sample project
* when a package is releasing a new patch (we like to update external dependencies in patches when possible)
-->
<UseLatestPackageReferences Condition=" '$(UseLatestPackageReferences)' == '' AND '$(IsServicingBuild)' != 'true' ">true</UseLatestPackageReferences>
<UseLatestPackageReferences Condition=" '$(UseLatestPackageReferences)' == '' AND '$(IsImplementationProject)' != 'true' ">true</UseLatestPackageReferences>
<UseLatestPackageReferences Condition=" '$(UseLatestPackageReferences)' == '' AND '$(IsImplementationProject)' == 'true' AND ( '$(IsServicingBuild)' != 'true' OR '$(IsPackable)' == 'true' ) ">true</UseLatestPackageReferences>
<UseLatestPackageReferences Condition=" '$(UseLatestPackageReferences)' == '' ">false</UseLatestPackageReferences>
<!--
Projects should only use the project references instead of baseline package references when:
* preparing a new major or minor release (i.e. a non-servicing builds)
* when a project is a test or sample project
We don't use project references between components in servicing builds between compontents to preserve the baseline as much as possible.
-->
<UseProjectReferences Condition=" '$(UseProjectReferences)' == '' AND '$(IsServicingBuild)' != 'true' ">true</UseProjectReferences>
<UseProjectReferences Condition=" '$(UseProjectReferences)' == '' AND '$(IsImplementationProject)' != 'true' ">true</UseProjectReferences>
<UseProjectReferences Condition=" '$(UseProjectReferences)' == '' ">false</UseProjectReferences>
</PropertyGroup>
<ItemGroup>
<_ImplicitPackageReference Include="@(PackageReference->WithMetadataValue('IsImplicitlyDefined', 'true'))" />
<_ExplicitPackageReference Include="@(PackageReference)" Exclude="@(_ImplicitPackageReference)" />
<_ExplicitPackageReference Remove="Internal.AspNetCore.Sdk" />
<UnusedProjectReferenceProvider Include="@(ProjectReferenceProvider)" Exclude="@(Reference)" />
<!-- Order matters. Projects should be used when possible instead of packages. -->
<_ProjectReferenceByAssemblyName Condition="'$(UseProjectReferences)' == 'true'"
Include="@(ProjectReferenceProvider)"
Exclude="@(UnusedProjectReferenceProvider)" />
<ProjectReference Include="@(_ProjectReferenceByAssemblyName->'%(ProjectPath)')" />
<Reference Remove="@(_ProjectReferenceByAssemblyName)" />
</ItemGroup>
<Target Name="ResolveCustomReferences" BeforeTargets="CollectPackageReferences;ResolveAssemblyReferencesDesignTime;ResolveAssemblyReferences" Condition=" '$(TargetFramework)' != '' ">
<ItemGroup>
<UnusedBaselinePackageReference Include="@(BaselinePackageReference)" Exclude="@(Reference);@(_ProjectReferenceByAssemblyName)" />
<!--
MSBuild does not provide a way to join on matching identities in a Condition,
but you can do a cartesian product of two item groups and filter out mismatched id's in a second pass.
-->
<_LatestPackageReferenceWithVersion Include="@(Reference)" Condition=" '$(UseLatestPackageReferences)' == 'true' ">
<Id>%(LatestPackageReference.Identity)</Id>
<Version>%(LatestPackageReference.Version)</Version>
</_LatestPackageReferenceWithVersion>
<_LatestPackageReferenceWithVersion Remove="@(_LatestPackageReferenceWithVersion)" Condition="'%(Id)' != '%(Identity)' " />
<!-- Remove reference items that have been resolved to a LatestPackageReference item. -->
<Reference Remove="@(_LatestPackageReferenceWithVersion)" />
<PackageReference Include="@(_LatestPackageReferenceWithVersion)" IsImplicitlyDefined="true" />
<!-- Resolve references from BaselinePackageReference for servicing builds. -->
<_BaselinePackageReferenceWithVersion Include="@(Reference)" Condition=" '$(IsServicingBuild)' == 'true' OR '$(UseLatestPackageReferences)' != 'true' ">
<Id>%(BaselinePackageReference.Identity)</Id>
<Version>%(BaselinePackageReference.Version)</Version>
</_BaselinePackageReferenceWithVersion>
<_BaselinePackageReferenceWithVersion Remove="@(_BaselinePackageReferenceWithVersion)" Condition="'%(Id)' != '%(Identity)' " />
<!-- Remove reference items that have been resolved to a BaselinePackageReference item. -->
<PackageReference Include="@(_BaselinePackageReferenceWithVersion)" IsImplicitlyDefined="true" />
<Reference Remove="@(_BaselinePackageReferenceWithVersion)" />
<!-- For PrivateAssets=All references, like .Sources packages, fallback to LatestPackageReferences. -->
<_PrivatePackageReferenceWithVersion Include="@(Reference->WithMetadataValue('PrivateAssets', 'All'))">
<Id>%(LatestPackageReference.Identity)</Id>
<Version>%(LatestPackageReference.Version)</Version>
</_PrivatePackageReferenceWithVersion>
<_PrivatePackageReferenceWithVersion Remove="@(_PrivatePackageReferenceWithVersion)" Condition="'%(Id)' != '%(Identity)' " />
<!-- Remove reference items that have been resolved to a LatestPackageReference item. -->
<PackageReference Include="@(_PrivatePackageReferenceWithVersion)" IsImplicitlyDefined="true" />
<Reference Remove="@(_PrivatePackageReferenceWithVersion)" />
<!-- Free up memory for unnecessary items -->
<_LatestPackageReferenceWithVersion Remove="@(_LatestPackageReferenceWithVersion)" />
<_BaselinePackageReferenceWithVersion Remove="@(_BaselinePackageReferenceWithVersion)" />
<_PrivatePackageReferenceWithVersion Remove="@(_PrivatePackageReferenceWithVersion)" />
<_ImplicitPackageReference Remove="@(_ImplicitPackageReference)" />
</ItemGroup>
<!--
<Error Condition="@(_ExplicitPackageReference->Count()) != 0"
Text="PackageReference items are not allowed. Use &lt;Reference&gt; instead. " /> -->
<ItemGroup>
<_ExplicitPackageReference Remove="@(_ExplicitPackageReference)" />
</ItemGroup>
<Warning Condition="@(UnusedBaselinePackageReference->Count()) != 0"
Text="Package references changed since the last release. This could be a breaking change. References removed:%0A - @(UnusedBaselinePackageReference, '%0A -')" />
<Error Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework' AND '%(Reference.Identity)' != '' AND ! Exists('%(Reference.Identity)')"
Code="MSB3245"
Text="Could not resolve this reference. Could not locate the package or project for &quot;%(Reference.Identity)&quot;" />
</Target>
<Target Name="GetReferencesProvided" Returns="@(ProvidesReference)">
<ItemGroup>
<_TargetFramework Remove="@(_TargetFramework)" />
<_TargetFramework Include="$(TargetFramework)" Condition="'$(TargetFramework)' != '' "/>
<_TargetFramework Include="$(TargetFrameworks)" Condition="'$(TargetFramework)' == '' "/>
</ItemGroup>
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_GetReferencesProvided"
Properties="TargetFramework=%(_TargetFramework.Identity)">
<Output TaskParameter="TargetOutputs" ItemName="ProvidesReference" />
</MSBuild>
</Target>
<Target Name="_GetReferencesProvided" Returns="@(ProvidesReference)">
<ItemGroup Condition=" '$(IsImplementationProject)' == 'true' OR '$(IsProjectReferenceProvider)' == 'true' ">
<ProvidesReference Include="$(AssemblyName)">
<ProjectFileRelativePath>$([MSBuild]::MakeRelative($(RepositoryRoot), $(MSBuildProjectFullPath)))</ProjectFileRelativePath>
</ProvidesReference>
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<StartArguments>-o "$(MSBuildThisFileDirectory)../../Baseline.props"</StartArguments>
<StartWorkingDirectory>$(MSBuildProjectDirectory)</StartWorkingDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NuGet.Packaging" Version="4.8.0" />
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.CommandLineUtils;
using NuGet.Packaging;
using NuGet.Packaging.Core;
namespace PackageBaselineGenerator
{
/// <summary>
/// This generates Baseline.props with information about the last RTM release.
/// </summary>
class Program : CommandLineApplication
{
static void Main(string[] args)
{
new Program().Execute(args);
}
private readonly CommandOption _source;
private readonly CommandOption _output;
public Program()
{
_source = Option("-s|--source <SOURCE>", "The NuGet v2 source of the package to fetch", CommandOptionType.SingleValue);
_output = Option("-o|--output <OUT>", "The generated file output path", CommandOptionType.SingleValue);
Invoke = () => Run().GetAwaiter().GetResult();
}
private async Task<int> Run()
{
var source = _source.HasValue()
? _source.Value()
: "https://www.nuget.org/api/v2/package";
var packageCache = Environment.GetEnvironmentVariable("NUGET_PACKAGES") != null
? Environment.GetEnvironmentVariable("NUGET_PACKAGES")
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
var tempDir = Path.Combine(Directory.GetCurrentDirectory(), "obj", "tmp");
Directory.CreateDirectory(tempDir);
var input = XDocument.Load(Path.Combine(Directory.GetCurrentDirectory(), "baseline.xml"));
var output = _output.HasValue()
? _output.Value()
: Path.Combine(Directory.GetCurrentDirectory(), "Baseline.props");
var baselineVersion = input.Root.Attribute("Version").Value;
var doc = new XDocument(
new XComment(" Auto generated. Do not edit manually, use eng/tools/BaselineGenerator/ to recreate. "),
new XElement("Project",
new XElement("PropertyGroup",
new XElement("MSBuildAllProjects", "$(MSBuildAllProjects);$(MSBuildThisFileFullPath)"),
new XElement("AspNetCoreBaselineVersion", baselineVersion))));
var client = new HttpClient();
foreach (var pkg in input.Root.Descendants("Package"))
{
var id = pkg.Attribute("Id").Value;
var version = pkg.Attribute("Version").Value;
var packageFileName = $"{id}.{version}.nupkg";
var nupkgPath = Path.Combine(packageCache, id.ToLowerInvariant(), version, packageFileName);
if (!File.Exists(nupkgPath))
{
nupkgPath = Path.Combine(tempDir, packageFileName);
}
if (!File.Exists(nupkgPath))
{
var url = $"{source}/{id}/{version}";
using (var file = File.Create(nupkgPath))
{
Console.WriteLine($"Downloading {url}");
var response = await client.GetStreamAsync(url);
await response.CopyToAsync(file);
}
}
using (var reader = new PackageArchiveReader(nupkgPath))
{
var first = true;
foreach (var group in reader.NuspecReader.GetDependencyGroups())
{
if (first)
{
first = false;
doc.Root.Add(new XComment($" Package: {id}"));
var propertyGroup = new XElement("PropertyGroup",
new XAttribute("Condition", $" '$(PackageId)' == '{id}' "),
new XElement("BaselinePackageVersion", version));
doc.Root.Add(propertyGroup);
}
var itemGroup = new XElement("ItemGroup", new XAttribute("Condition", $" '$(PackageId)' == '{id}' AND '$(TargetFramework)' == '{group.TargetFramework.GetShortFolderName()}' "));
doc.Root.Add(itemGroup);
foreach (var dependency in group.Packages)
{
itemGroup.Add(new XElement("BaselinePackageReference", new XAttribute("Include", dependency.Id), new XAttribute("Version", dependency.VersionRange.ToString())));
}
}
}
}
var settings = new XmlWriterSettings
{
OmitXmlDeclaration = true,
Encoding = Encoding.UTF8,
Indent = true,
};
using (var writer = XmlWriter.Create(output, settings))
{
doc.Save(writer);
}
return 0;
}
}
}

View File

@ -0,0 +1,10 @@
BaselineGenerator
=================
This tool is used to generate an MSBuild file which sets the "baseline" against which servicing updates are built.
## Usage
1. Add to the [baseline.xml](./baseline.xml) a list of package ID's and their latest released versions. The source of this information can typically
be found in the build.xml file generated during ProdCon builds. See https://github.com/dotnet/versions/blob/master/build-info/dotnet/product/cli/release/2.1.6/build.xml for example.
2. Run `dotnet run` on this project.

View File

@ -0,0 +1,13 @@
<Baseline Version="2.1.6">
<Package Id="Microsoft.AspNetCore.Cryptography.Internal" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.Abstractions" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.AzureKeyVault" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.Extensions" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.Redis" Version="0.4.1" />
<Package Id="Microsoft.AspNetCore.DataProtection.SystemWeb" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.JsonPatch" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.WebSockets" Version="2.1.1" />
</Baseline>

View File

@ -0,0 +1,3 @@
<Project>
<Import Project="..\..\build\sources.props" />
</Project>

View File

@ -0,0 +1,2 @@
<Project>
</Project>

34
eng/tools/tools.sln Normal file
View File

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaselineGenerator", "BaselineGenerator\BaselineGenerator.csproj", "{CF76A947-3A72-4824-87E6-BF029D84218B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|x64.ActiveCfg = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|x64.Build.0 = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|x86.ActiveCfg = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Debug|x86.Build.0 = Debug|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|Any CPU.Build.0 = Release|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|x64.ActiveCfg = Release|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|x64.Build.0 = Release|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|x86.ActiveCfg = Release|Any CPU
{CF76A947-3A72-4824-87E6-BF029D84218B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

@ -1 +0,0 @@
Subproject commit 218064c300a7cf5a76669e133340a98a0c5517a5

View File

@ -5,20 +5,4 @@
<Import Project="version.props" />
<Import Project="dependencies.props" />
<PropertyGroup>
<VerifyVersion>false</VerifyVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
</ItemGroup>
<ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
<PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,112 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Operations;
namespace Microsoft.AspNetCore.JsonPatch.Adapters
{
/// <summary>
/// Defines the operations that can be performed on a JSON patch document.
/// </summary>
public interface IObjectAdapter
{
/// <summary>
/// Using the "add" operation a new value is inserted into the root of the target
/// document, into the target array at the specified valid index, or to a target object at
/// the specified location.
///
/// When adding to arrays, the specified index MUST NOT be greater than the number of elements in the array.
/// To append the value to the array, the index of "-" character is used (see [RFC6901]).
///
/// When adding to an object, if an object member does not already exist, a new member is added to the object at the
/// specified location or if an object member does exist, that member's value is replaced.
///
/// The operation object MUST contain a "value" member whose content
/// specifies the value to be added.
///
/// For example:
///
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-4
/// </summary>
/// <param name="operation">The add operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Add(Operation operation, object objectToApplyTo);
/// <summary>
/// Using the "copy" operation, a value is copied from a specified location to the
/// target location.
///
/// The operation object MUST contain a "from" member, which references the location in the
/// target document to copy the value from.
///
/// The "from" location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7
/// </summary>
/// <param name="operation">The copy operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Copy(Operation operation, object objectToApplyTo);
/// <summary>
/// Using the "move" operation the value at a specified location is removed and
/// added to the target location.
///
/// The operation object MUST contain a "from" member, which references the location in the
/// target document to move the value from.
///
/// The "from" location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
///
/// A location cannot be moved into one of its children.
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6
/// </summary>
/// <param name="operation">The move operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Move(Operation operation, object objectToApplyTo);
/// <summary>
/// Using the "remove" operation the value at the target location is removed.
///
/// The target location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "remove", "path": "/a/b/c" }
///
/// If removing an element from an array, any elements above the
/// specified index are shifted one position to the left.
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6
/// </summary>
/// <param name="operation">The remove operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Remove(Operation operation, object objectToApplyTo);
/// <summary>
/// Using the "replace" operation he value at the target location is replaced
/// with a new value. The operation object MUST contain a "value" member
/// which specifies the replacement value.
///
/// The target location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6
/// </summary>
/// <param name="operation">The replace operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Replace(Operation operation, object objectToApplyTo);
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Operations;
namespace Microsoft.AspNetCore.JsonPatch.Adapters
{
/// <summary>
/// Defines the operations that can be performed on a JSON patch document, including "test".
/// </summary>
public interface IObjectAdapterWithTest : IObjectAdapter
{
/// <summary>
/// Using the "test" operation a value at the target location is compared for
/// equality to a specified value.
///
/// The operation object MUST contain a "value" member that specifies
/// value to be compared to the target location's value.
///
/// The target location MUST be equal to the "value" value for the
/// operation to be considered successful.
///
/// For example:
/// { "op": "test", "path": "/a/b/c", "value": "foo" }
///
/// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7
/// </summary>
/// <param name="operation">The test operation.</param>
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
void Test(Operation operation, object objectToApplyTo);
}
}

View File

@ -0,0 +1,328 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Adapters
{
/// <inheritdoc />
public class ObjectAdapter : IObjectAdapterWithTest
{
/// <summary>
/// Initializes a new instance of <see cref="ObjectAdapter"/>.
/// </summary>
/// <param name="contractResolver">The <see cref="IContractResolver"/>.</param>
/// <param name="logErrorAction">The <see cref="Action"/> for logging <see cref="JsonPatchError"/>.</param>
public ObjectAdapter(
IContractResolver contractResolver,
Action<JsonPatchError> logErrorAction)
{
ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver));
LogErrorAction = logErrorAction;
}
/// <summary>
/// Gets or sets the <see cref="IContractResolver"/>.
/// </summary>
public IContractResolver ContractResolver { get; }
/// <summary>
/// Action for logging <see cref="JsonPatchError"/>.
/// </summary>
public Action<JsonPatchError> LogErrorAction { get; }
public void Add(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
Add(operation.path, operation.value, objectToApplyTo, operation);
}
/// <summary>
/// Add is used by various operations (eg: add, copy, ...), yet through different operations;
/// This method allows code reuse yet reporting the correct operation on error
/// </summary>
private void Add(
string path,
object value,
object objectToApplyTo,
Operation operation)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
var parsedPath = new ParsedPath(path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
var target = objectToApplyTo;
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
{
var error = CreatePathNotFoundError(objectToApplyTo, path, operation, errorMessage);
ErrorReporter(error);
return;
}
if (!adapter.TryAdd(target, parsedPath.LastSegment, ContractResolver, value, out errorMessage))
{
var error = CreateOperationFailedError(objectToApplyTo, path, operation, errorMessage);
ErrorReporter(error);
return;
}
}
public void Move(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
// Get value at 'from' location and add that value to the 'path' location
if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue))
{
// remove that value
Remove(operation.from, objectToApplyTo, operation);
// add that value to the path location
Add(operation.path,
propertyValue,
objectToApplyTo,
operation);
}
}
public void Remove(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
Remove(operation.path, objectToApplyTo, operation);
}
/// <summary>
/// Remove is used by various operations (eg: remove, move, ...), yet through different operations;
/// This method allows code reuse yet reporting the correct operation on error. The return value
/// contains the type of the item that has been removed (and a bool possibly signifying an error)
/// This can be used by other methods, like replace, to ensure that we can pass in the correctly
/// typed value to whatever method follows.
/// </summary>
private void Remove(string path, object objectToApplyTo, Operation operationToReport)
{
var parsedPath = new ParsedPath(path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
var target = objectToApplyTo;
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
{
var error = CreatePathNotFoundError(objectToApplyTo, path, operationToReport, errorMessage);
ErrorReporter(error);
return;
}
if (!adapter.TryRemove(target, parsedPath.LastSegment, ContractResolver, out errorMessage))
{
var error = CreateOperationFailedError(objectToApplyTo, path, operationToReport, errorMessage);
ErrorReporter(error);
return;
}
}
public void Replace(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var parsedPath = new ParsedPath(operation.path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
var target = objectToApplyTo;
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
{
var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage);
ErrorReporter(error);
return;
}
if (!adapter.TryReplace(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage))
{
var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage);
ErrorReporter(error);
return;
}
}
public void Copy(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
// Get value at 'from' location and add that value to the 'path' location
if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue))
{
// Create deep copy
var copyResult = ConversionResultProvider.CopyTo(propertyValue, propertyValue.GetType());
if (copyResult.CanBeConverted)
{
Add(operation.path,
copyResult.ConvertedInstance,
objectToApplyTo,
operation);
}
else
{
var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, Resources.FormatCannotCopyProperty(operation.from));
ErrorReporter(error);
return;
}
}
}
public void Test(Operation operation, object objectToApplyTo)
{
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var parsedPath = new ParsedPath(operation.path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
var target = objectToApplyTo;
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
{
var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage);
ErrorReporter(error);
return;
}
if (!adapter.TryTest(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage))
{
var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage);
ErrorReporter(error);
return;
}
}
private bool TryGetValue(
string fromLocation,
object objectToGetValueFrom,
Operation operation,
out object propertyValue)
{
if (fromLocation == null)
{
throw new ArgumentNullException(nameof(fromLocation));
}
if (objectToGetValueFrom == null)
{
throw new ArgumentNullException(nameof(objectToGetValueFrom));
}
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
propertyValue = null;
var parsedPath = new ParsedPath(fromLocation);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
var target = objectToGetValueFrom;
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
{
var error = CreatePathNotFoundError(objectToGetValueFrom, fromLocation, operation, errorMessage);
ErrorReporter(error);
return false;
}
if (!adapter.TryGet(target, parsedPath.LastSegment, ContractResolver, out propertyValue, out errorMessage))
{
var error = CreateOperationFailedError(objectToGetValueFrom, fromLocation, operation, errorMessage);
ErrorReporter(error);
return false;
}
return true;
}
private Action<JsonPatchError> ErrorReporter
{
get
{
return LogErrorAction ?? Internal.ErrorReporter.Default;
}
}
private JsonPatchError CreateOperationFailedError(object target, string path, Operation operation, string errorMessage)
{
return new JsonPatchError(
target,
operation,
errorMessage ?? Resources.FormatCannotPerformOperation(operation.op, path));
}
private JsonPatchError CreatePathNotFoundError(object target, string path, Operation operation, string errorMessage)
{
return new JsonPatchError(
target,
operation,
errorMessage ?? Resources.FormatTargetLocationNotFound(operation.op, path));
}
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Converters
{
public class JsonPatchDocumentConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
if (objectType != typeof(JsonPatchDocument))
{
throw new ArgumentException(Resources.FormatParameterMustMatchType("objectType", "JsonPatchDocument"), "objectType");
}
try
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
// load jObject
var jObject = JArray.Load(reader);
// Create target object for Json => list of operations
var targetOperations = new List<Operation>();
// Create a new reader for this jObject, and set all properties
// to match the original reader.
var jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
// Populate the object properties
serializer.Populate(jObjectReader, targetOperations);
// container target: the JsonPatchDocument.
var container = new JsonPatchDocument(targetOperations, new DefaultContractResolver());
return container;
}
catch (Exception ex)
{
throw new JsonSerializationException(Resources.InvalidJsonPatchDocument, ex);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is IJsonPatchDocument)
{
var jsonPatchDoc = (IJsonPatchDocument)value;
var lst = jsonPatchDoc.GetOperations();
// write out the operations, no envelope
serializer.Serialize(writer, lst);
}
}
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Converters
{
public class TypedJsonPatchDocumentConverter : JsonPatchDocumentConverter
{
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var genericType = objectType.GetTypeInfo().GenericTypeArguments[0];
// load jObject
var jObject = JArray.Load(reader);
// Create target object for Json => list of operations, typed to genericType
var genericOperation = typeof(Operation<>);
var concreteOperationType = genericOperation.MakeGenericType(genericType);
var genericList = typeof(List<>);
var concreteList = genericList.MakeGenericType(concreteOperationType);
var targetOperations = Activator.CreateInstance(concreteList);
//Create a new reader for this jObject, and set all properties to match the original reader.
var jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
// Populate the object properties
serializer.Populate(jObjectReader, targetOperations);
// container target: the typed JsonPatchDocument.
var container = Activator.CreateInstance(objectType, targetOperations, new DefaultContractResolver());
return container;
}
catch (Exception ex)
{
throw new JsonSerializationException(Resources.InvalidJsonPatchDocument, ex);
}
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Operations;
namespace Microsoft.AspNetCore.JsonPatch.Exceptions
{
public class JsonPatchException : Exception
{
public Operation FailedOperation { get; private set; }
public object AffectedObject { get; private set; }
public JsonPatchException()
{
}
public JsonPatchException(JsonPatchError jsonPatchError, Exception innerException)
: base(jsonPatchError.ErrorMessage, innerException)
{
FailedOperation = jsonPatchError.Operation;
AffectedObject = jsonPatchError.AffectedObject;
}
public JsonPatchException(JsonPatchError jsonPatchError)
: this(jsonPatchError, null)
{
}
public JsonPatchException(string message, Exception innerException)
: base (message, innerException)
{
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Helpers
{
/// <summary>
/// Return value for the helper method used by Copy/Move. Needed to ensure we can make a different
/// decision in the calling method when the value is null because it cannot be fetched (HasError = true)
/// versus when it actually is null (much like why RemovedPropertyTypeResult is used for returning
/// type in the Remove operation).
/// </summary>
public class GetValueResult
{
public GetValueResult(object propertyValue, bool hasError)
{
PropertyValue = propertyValue;
HasError = hasError;
}
/// <summary>
/// The value of the property we're trying to get
/// </summary>
public object PropertyValue { get; private set; }
/// <summary>
/// HasError: true when an error occurred, the operation didn't complete succesfully
/// </summary>
public bool HasError { get; private set; }
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch
{
/// <summary>
/// Metadata for JsonProperty.
/// </summary>
public class JsonPatchProperty
{
/// <summary>
/// Initializes a new instance.
/// </summary>
public JsonPatchProperty(JsonProperty property, object parent)
{
if (property == null)
{
throw new ArgumentNullException(nameof(property));
}
if (parent == null)
{
throw new ArgumentNullException(nameof(parent));
}
Property = property;
Parent = parent;
}
/// <summary>
/// Gets or sets JsonProperty.
/// </summary>
public JsonProperty Property { get; set; }
/// <summary>
/// Gets or sets Parent.
/// </summary>
public object Parent { get; set; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Operations;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch
{
public interface IJsonPatchDocument
{
IContractResolver ContractResolver { get; set; }
IList<Operation> GetOperations();
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ConversionResult
{
public ConversionResult(bool canBeConverted, object convertedInstance)
{
CanBeConverted = canBeConverted;
ConvertedInstance = convertedInstance;
}
public bool CanBeConverted { get; }
public object ConvertedInstance { get; }
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public static class ConversionResultProvider
{
public static ConversionResult ConvertTo(object value, Type typeToConvertTo)
{
if (value == null)
{
return new ConversionResult(IsNullableType(typeToConvertTo), null);
}
else if (typeToConvertTo.IsAssignableFrom(value.GetType()))
{
// No need to convert
return new ConversionResult(true, value);
}
else
{
try
{
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo);
return new ConversionResult(true, deserialized);
}
catch
{
return new ConversionResult(canBeConverted: false, convertedInstance: null);
}
}
}
public static ConversionResult CopyTo(object value, Type typeToConvertTo)
{
var targetType = typeToConvertTo;
if (value == null)
{
return new ConversionResult(IsNullableType(typeToConvertTo), null);
}
else if (typeToConvertTo.IsAssignableFrom(value.GetType()))
{
// Keep original type
targetType = value.GetType();
}
try
{
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), targetType);
return new ConversionResult(true, deserialized);
}
catch
{
return new ConversionResult(canBeConverted: false, convertedInstance: null);
}
}
private static bool IsNullableType(Type type)
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsValueType)
{
// value types are only nullable if they are Nullable<T>
return typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
else
{
// reference types are always nullable
return true;
}
}
}
}

View File

@ -0,0 +1,245 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DictionaryAdapter<TKey, TValue> : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
// As per JsonPatch spec, if a key already exists, adding should replace the existing value
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, out var convertedValue, out errorMessage))
{
return false;
}
dictionary[convertedKey] = convertedValue;
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
value = null;
return false;
}
if (!dictionary.ContainsKey(convertedKey))
{
value = null;
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
value = dictionary[convertedKey];
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
return false;
}
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.ContainsKey(convertedKey))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
dictionary.Remove(convertedKey);
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
return false;
}
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.ContainsKey(convertedKey))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!TryConvertValue(value, out var convertedValue, out errorMessage))
{
return false;
}
dictionary[convertedKey] = convertedValue;
errorMessage = null;
return true;
}
public bool TryTest(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
return false;
}
// As per JsonPatch spec, the target location must exist for test to be successful
if (!dictionary.ContainsKey(convertedKey))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!TryConvertValue(value, out var convertedValue, out errorMessage))
{
return false;
}
var currentValue = dictionary[convertedKey];
// The target segment does not have an assigned value to compare the test value with
if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
{
errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment);
return false;
}
if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue)))
{
errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment);
return false;
}
else
{
errorMessage = null;
return true;
}
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage)
{
var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType());
var key = contract.DictionaryKeyResolver(segment);
var dictionary = (IDictionary<TKey, TValue>)target;
if (!TryConvertKey(key, out var convertedKey, out errorMessage))
{
nextTarget = null;
return false;
}
if (dictionary.ContainsKey(convertedKey))
{
nextTarget = dictionary[convertedKey];
errorMessage = null;
return true;
}
else
{
nextTarget = null;
errorMessage = null;
return false;
}
}
private bool TryConvertKey(string key, out TKey convertedKey, out string errorMessage)
{
var conversionResult = ConversionResultProvider.ConvertTo(key, typeof(TKey));
if (conversionResult.CanBeConverted)
{
errorMessage = null;
convertedKey = (TKey)conversionResult.ConvertedInstance;
return true;
}
else
{
errorMessage = Resources.FormatInvalidPathSegment(key);
convertedKey = default(TKey);
return false;
}
}
private bool TryConvertValue(object value, out TValue convertedValue, out string errorMessage)
{
var conversionResult = ConversionResultProvider.ConvertTo(value, typeof(TValue));
if (conversionResult.CanBeConverted)
{
errorMessage = null;
convertedValue = (TValue)conversionResult.ConvertedInstance;
return true;
}
else
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
convertedValue = default(TValue);
return false;
}
}
}
}

View File

@ -0,0 +1,243 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using CSharpBinder = Microsoft.CSharp.RuntimeBinder;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DynamicObjectAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
if (!TrySetDynamicObjectProperty(target, contractResolver, segment, value, out errorMessage))
{
return false;
}
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out value, out errorMessage))
{
value = null;
return false;
}
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage))
{
return false;
}
// Setting the value to "null" will use the default value in case of value types, and
// null in case of reference types
object value = null;
if (property.GetType().GetTypeInfo().IsValueType
&& Nullable.GetUnderlyingType(property.GetType()) == null)
{
value = Activator.CreateInstance(property.GetType());
}
if (!TrySetDynamicObjectProperty(target, contractResolver, segment, value, out errorMessage))
{
return false;
}
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, property.GetType(), out var convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
if (!TrySetDynamicObjectProperty(target, contractResolver, segment, convertedValue, out errorMessage))
{
return false;
}
errorMessage = null;
return true;
}
public bool TryTest(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, property.GetType(), out var convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
if (!JToken.DeepEquals(JsonConvert.SerializeObject(property), JsonConvert.SerializeObject(convertedValue)))
{
errorMessage = Resources.FormatValueNotEqualToTestValue(property, value, segment);
return false;
}
else
{
errorMessage = null;
return true;
}
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage)
{
if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage))
{
nextTarget = null;
return false;
}
else
{
nextTarget = property;
errorMessage = null;
return true;
}
}
private bool TryGetDynamicObjectProperty(
object target,
IContractResolver contractResolver,
string segment,
out object value,
out string errorMessage)
{
var jsonDynamicContract = (JsonDynamicContract)contractResolver.ResolveContract(target.GetType());
var propertyName = jsonDynamicContract.PropertyNameResolver(segment);
var binder = CSharpBinder.Binder.GetMember(
CSharpBinderFlags.None,
propertyName,
target.GetType(),
new List<CSharpArgumentInfo>
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
});
var callsite = CallSite<Func<CallSite, object, object>>.Create(binder);
try
{
value = callsite.Target(callsite, target);
errorMessage = null;
return true;
}
catch (RuntimeBinderException)
{
value = null;
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
}
private bool TrySetDynamicObjectProperty(
object target,
IContractResolver contractResolver,
string segment,
object value,
out string errorMessage)
{
var jsonDynamicContract = (JsonDynamicContract)contractResolver.ResolveContract(target.GetType());
var propertyName = jsonDynamicContract.PropertyNameResolver(segment);
var binder = CSharpBinder.Binder.SetMember(
CSharpBinderFlags.None,
propertyName,
target.GetType(),
new List<CSharpArgumentInfo>
{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
});
var callsite = CallSite<Func<CallSite, object, object, object>>.Create(binder);
try
{
callsite.Target(callsite, target, value);
errorMessage = null;
return true;
}
catch (RuntimeBinderException)
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
}
private bool TryConvertValue(object value, Type propertyType, out object convertedValue)
{
var conversionResult = ConversionResultProvider.ConvertTo(value, propertyType);
if (!conversionResult.CanBeConverted)
{
convertedValue = null;
return false;
}
convertedValue = conversionResult.ConvertedInstance;
return true;
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal static class ErrorReporter
{
public static readonly Action<JsonPatchError> Default = (error) =>
{
throw new JsonPatchException(error);
};
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public interface IAdapter
{
bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage);
bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage);
bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage);
bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage);
bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage);
bool TryTest(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage);
}
}

View File

@ -0,0 +1,349 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ListAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var list = (IList)target;
if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage))
{
return false;
}
if (!TryGetPositionInfo(list, segment, OperationType.Add, out var positionInfo, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list.Add(convertedValue);
}
else
{
list.Insert(positionInfo.Index, convertedValue);
}
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var list = (IList)target;
if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage))
{
value = null;
return false;
}
if (!TryGetPositionInfo(list, segment, OperationType.Get, out var positionInfo, out errorMessage))
{
value = null;
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
value = list[list.Count - 1];
}
else
{
value = list[positionInfo.Index];
}
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
var list = (IList)target;
if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage))
{
return false;
}
if (!TryGetPositionInfo(list, segment, OperationType.Remove, out var positionInfo, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list.RemoveAt(list.Count - 1);
}
else
{
list.RemoveAt(positionInfo.Index);
}
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var list = (IList)target;
if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage))
{
return false;
}
if (!TryGetPositionInfo(list, segment, OperationType.Replace, out var positionInfo, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list[list.Count - 1] = convertedValue;
}
else
{
list[positionInfo.Index] = convertedValue;
}
errorMessage = null;
return true;
}
public bool TryTest(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var list = (IList)target;
if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage))
{
return false;
}
if (!TryGetPositionInfo(list, segment, OperationType.Replace, out var positionInfo, out errorMessage))
{
return false;
}
if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage))
{
return false;
}
var currentValue = list[positionInfo.Index];
if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue)))
{
errorMessage = Resources.FormatValueAtListPositionNotEqualToTestValue(currentValue, value, positionInfo.Index);
return false;
}
else
{
errorMessage = null;
return true;
}
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var list = target as IList;
if (list == null)
{
value = null;
errorMessage = null;
return false;
}
var index = -1;
if (!int.TryParse(segment, out index))
{
value = null;
errorMessage = Resources.FormatInvalidIndexValue(segment);
return false;
}
if (index < 0 || index >= list.Count)
{
value = null;
errorMessage = Resources.FormatIndexOutOfBounds(segment);
return false;
}
value = list[index];
errorMessage = null;
return true;
}
private bool TryConvertValue(
object originalValue,
Type listTypeArgument,
string segment,
out object convertedValue,
out string errorMessage)
{
var conversionResult = ConversionResultProvider.ConvertTo(originalValue, listTypeArgument);
if (!conversionResult.CanBeConverted)
{
convertedValue = null;
errorMessage = Resources.FormatInvalidValueForProperty(originalValue);
return false;
}
convertedValue = conversionResult.ConvertedInstance;
errorMessage = null;
return true;
}
private bool TryGetListTypeArgument(IList list, out Type listTypeArgument, out string errorMessage)
{
// Arrays are not supported as they have fixed size and operations like Add, Insert do not make sense
var listType = list.GetType();
if (listType.IsArray)
{
errorMessage = Resources.FormatPatchNotSupportedForArrays(listType.FullName);
listTypeArgument = null;
return false;
}
else
{
var genericList = ClosedGenericMatcher.ExtractGenericInterface(listType, typeof(IList<>));
if (genericList == null)
{
errorMessage = Resources.FormatPatchNotSupportedForNonGenericLists(listType.FullName);
listTypeArgument = null;
return false;
}
else
{
listTypeArgument = genericList.GenericTypeArguments[0];
errorMessage = null;
return true;
}
}
}
private bool TryGetPositionInfo(
IList list,
string segment,
OperationType operationType,
out PositionInfo positionInfo,
out string errorMessage)
{
if (segment == "-")
{
positionInfo = new PositionInfo(PositionType.EndOfList, -1);
errorMessage = null;
return true;
}
var position = -1;
if (int.TryParse(segment, out position))
{
if (position >= 0 && position < list.Count)
{
positionInfo = new PositionInfo(PositionType.Index, position);
errorMessage = null;
return true;
}
// As per JSON Patch spec, for Add operation the index value representing the number of elements is valid,
// where as for other operations like Remove, Replace, Move and Copy the target index MUST exist.
else if (position == list.Count && operationType == OperationType.Add)
{
positionInfo = new PositionInfo(PositionType.EndOfList, -1);
errorMessage = null;
return true;
}
else
{
positionInfo = new PositionInfo(PositionType.OutOfBounds, position);
errorMessage = Resources.FormatIndexOutOfBounds(segment);
return false;
}
}
else
{
positionInfo = new PositionInfo(PositionType.Invalid, -1);
errorMessage = Resources.FormatInvalidIndexValue(segment);
return false;
}
}
private struct PositionInfo
{
public PositionInfo(PositionType type, int index)
{
Type = type;
Index = index;
}
public PositionType Type { get; }
public int Index { get; }
}
private enum PositionType
{
Index, // valid index
EndOfList, // '-'
Invalid, // Ex: not an integer
OutOfBounds
}
private enum OperationType
{
Add,
Remove,
Get,
Replace
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitor
{
private readonly IContractResolver _contractResolver;
private readonly ParsedPath _path;
public ObjectVisitor(ParsedPath path, IContractResolver contractResolver)
{
_path = path;
_contractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver));
}
public bool TryVisit(ref object target, out IAdapter adapter, out string errorMessage)
{
if (target == null)
{
adapter = null;
errorMessage = null;
return false;
}
adapter = SelectAdapter(target);
// Traverse until the penultimate segment to get the target object and adapter
for (var i = 0; i < _path.Segments.Count - 1; i++)
{
if (!adapter.TryTraverse(target, _path.Segments[i], _contractResolver, out var next, out errorMessage))
{
adapter = null;
return false;
}
target = next;
adapter = SelectAdapter(target);
}
errorMessage = null;
return true;
}
private IAdapter SelectAdapter(object targetObject)
{
var jsonContract = _contractResolver.ResolveContract(targetObject.GetType());
if (targetObject is IList)
{
return new ListAdapter();
}
else if (jsonContract is JsonDictionaryContract jsonDictionaryContract)
{
var type = typeof(DictionaryAdapter<,>).MakeGenericType(jsonDictionaryContract.DictionaryKeyType, jsonDictionaryContract.DictionaryValueType);
return (IAdapter)Activator.CreateInstance(type);
}
else if (jsonContract is JsonDynamicContract)
{
return new DynamicObjectAdapter();
}
else
{
return new PocoAdapter();
}
}
}
}

View File

@ -0,0 +1,92 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public struct ParsedPath
{
private static readonly string[] Empty = null;
private readonly string[] _segments;
public ParsedPath(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
_segments = ParsePath(path);
}
public string LastSegment
{
get
{
if (_segments == null || _segments.Length == 0)
{
return null;
}
return _segments[_segments.Length - 1];
}
}
public IReadOnlyList<string> Segments => _segments ?? Empty;
private static string[] ParsePath(string path)
{
var strings = new List<string>();
var sb = new StringBuilder(path.Length);
for (var i = 0; i < path.Length; i++)
{
if (path[i] == '/')
{
if (sb.Length > 0)
{
strings.Add(sb.ToString());
sb.Length = 0;
}
}
else if (path[i] == '~')
{
++i;
if (i >= path.Length)
{
throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null);
}
if (path[i] == '0')
{
sb.Append('~');
}
else if (path[i] == '1')
{
sb.Append('/');
}
else
{
throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null);
}
}
else
{
sb.Append(path[i]);
}
}
if (sb.Length > 0)
{
strings.Add(sb.ToString());
}
return strings.ToArray();
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using System;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal static class PathHelpers
{
internal static string ValidateAndNormalizePath(string path)
{
// check for most common path errors on create. This is not
// absolutely necessary, but it allows us to already catch mistakes
// on creation of the patch document rather than on execute.
if (path.Contains("//"))
{
throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null);
}
if (!path.StartsWith("/", StringComparison.Ordinal))
{
return "/" + path;
}
else
{
return path;
}
}
}
}

View File

@ -0,0 +1,236 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class PocoAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
jsonProperty.ValueProvider.SetValue(target, convertedValue);
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
value = null;
return false;
}
if (!jsonProperty.Readable)
{
errorMessage = Resources.FormatCannotReadProperty(segment);
value = null;
return false;
}
value = jsonProperty.ValueProvider.GetValue(target);
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
// Setting the value to "null" will use the default value in case of value types, and
// null in case of reference types
object value = null;
if (jsonProperty.PropertyType.GetTypeInfo().IsValueType
&& Nullable.GetUnderlyingType(jsonProperty.PropertyType) == null)
{
value = Activator.CreateInstance(jsonProperty.PropertyType);
}
jsonProperty.ValueProvider.SetValue(target, value);
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver
contractResolver,
object value,
out string errorMessage)
{
if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
jsonProperty.ValueProvider.SetValue(target, convertedValue);
errorMessage = null;
return true;
}
public bool TryTest(
object target,
string segment,
IContractResolver
contractResolver,
object value,
out string errorMessage)
{
if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Readable)
{
errorMessage = Resources.FormatCannotReadProperty(segment);
return false;
}
if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
var currentValue = jsonProperty.ValueProvider.GetValue(target);
if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue)))
{
errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment);
return false;
}
errorMessage = null;
return true;
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
if (target == null)
{
value = null;
errorMessage = null;
return false;
}
if (TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty))
{
value = jsonProperty.ValueProvider.GetValue(target);
errorMessage = null;
return true;
}
value = null;
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
private bool TryGetJsonProperty(
object target,
IContractResolver contractResolver,
string segment,
out JsonProperty jsonProperty)
{
if (contractResolver.ResolveContract(target.GetType()) is JsonObjectContract jsonObjectContract)
{
var pocoProperty = jsonObjectContract
.Properties
.FirstOrDefault(p => string.Equals(p.PropertyName, segment, StringComparison.OrdinalIgnoreCase));
if (pocoProperty != null)
{
jsonProperty = pocoProperty;
return true;
}
}
jsonProperty = null;
return false;
}
private bool TryConvertValue(object value, Type propertyType, out object convertedValue)
{
var conversionResult = ConversionResultProvider.ConvertTo(value, propertyType);
if (!conversionResult.CanBeConverted)
{
convertedValue = null;
return false;
}
convertedValue = conversionResult.ConvertedInstance;
return true;
}
}
}

View File

@ -0,0 +1,256 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch
{
// Implementation details: the purpose of this type of patch document is to allow creation of such
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
// .NET or architecture doesn't contain a shared DTO layer.
[JsonConverter(typeof(JsonPatchDocumentConverter))]
public class JsonPatchDocument : IJsonPatchDocument
{
public List<Operation> Operations { get; private set; }
[JsonIgnore]
public IContractResolver ContractResolver { get; set; }
public JsonPatchDocument()
{
Operations = new List<Operation>();
ContractResolver = new DefaultContractResolver();
}
public JsonPatchDocument(List<Operation> operations, IContractResolver contractResolver)
{
if (operations == null)
{
throw new ArgumentNullException(nameof(operations));
}
if (contractResolver == null)
{
throw new ArgumentNullException(nameof(contractResolver));
}
Operations = operations;
ContractResolver = contractResolver;
}
/// <summary>
/// Add operation. Will result in, for example,
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
/// </summary>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument Add(string path, object value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("add", PathHelpers.ValidateAndNormalizePath(path), null, value));
return this;
}
/// <summary>
/// Remove value at target location. Will result in, for example,
/// { "op": "remove", "path": "/a/b/c" }
/// </summary>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument Remove(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("remove", PathHelpers.ValidateAndNormalizePath(path), null, null));
return this;
}
/// <summary>
/// Replace value. Will result in, for example,
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument Replace(string path, object value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("replace", PathHelpers.ValidateAndNormalizePath(path), null, value));
return this;
}
/// <summary>
/// Test value. Will result in, for example,
/// { "op": "test", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument Test(string path, object value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("test", PathHelpers.ValidateAndNormalizePath(path), null, value));
return this;
}
/// <summary>
/// Removes value at specified location and add it to the target location. Will result in, for example:
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
/// </summary>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument Move(string from, string path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("move", PathHelpers.ValidateAndNormalizePath(path), PathHelpers.ValidateAndNormalizePath(from)));
return this;
}
/// <summary>
/// Copy the value at specified location to the target location. Will result in, for example:
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// </summary>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument Copy(string from, string path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation("copy", PathHelpers.ValidateAndNormalizePath(path), PathHelpers.ValidateAndNormalizePath(from)));
return this;
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
public void ApplyTo(object objectToApplyTo)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null));
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="logErrorAction">Action to log errors</param>
public void ApplyTo(object objectToApplyTo, Action<JsonPatchError> logErrorAction)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var adapter = new ObjectAdapter(ContractResolver, logErrorAction);
foreach (var op in Operations)
{
try
{
op.Apply(objectToApplyTo, adapter);
}
catch (JsonPatchException jsonPatchException)
{
var errorReporter = logErrorAction ?? ErrorReporter.Default;
errorReporter(new JsonPatchError(objectToApplyTo, op, jsonPatchException.Message));
// As per JSON Patch spec if an operation results in error, further operations should not be executed.
break;
}
}
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="adapter">IObjectAdapter instance to use when applying</param>
public void ApplyTo(object objectToApplyTo, IObjectAdapter adapter)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (adapter == null)
{
throw new ArgumentNullException(nameof(adapter));
}
// apply each operation in order
foreach (var op in Operations)
{
op.Apply(objectToApplyTo, adapter);
}
}
IList<Operation> IJsonPatchDocument.GetOperations()
{
var allOps = new List<Operation>();
if (Operations != null)
{
foreach (var op in Operations)
{
var untypedOp = new Operation();
untypedOp.op = op.op;
untypedOp.value = op.value;
untypedOp.path = op.path;
untypedOp.from = op.from;
allOps.Add(untypedOp);
}
}
return allOps;
}
}
}

View File

@ -0,0 +1,869 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch
{
// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking
// when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
// not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
{
public List<Operation<TModel>> Operations { get; private set; }
[JsonIgnore]
public IContractResolver ContractResolver { get; set; }
public JsonPatchDocument()
{
Operations = new List<Operation<TModel>>();
ContractResolver = new DefaultContractResolver();
}
// Create from list of operations
public JsonPatchDocument(List<Operation<TModel>> operations, IContractResolver contractResolver)
{
Operations = operations ?? throw new ArgumentNullException(nameof(operations));
ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver));
}
/// <summary>
/// Add operation. Will result in, for example,
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Add<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"add",
GetPath(path, null),
from: null,
value: value));
return this;
}
/// <summary>
/// Add value to list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Add<TProp>(
Expression<Func<TModel, IList<TProp>>> path,
TProp value,
int position)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"add",
GetPath(path, position.ToString()),
from: null,
value: value));
return this;
}
/// <summary>
/// Add value to the end of the list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Add<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"add",
GetPath(path, "-"),
from: null,
value: value));
return this;
}
/// <summary>
/// Remove value at target location. Will result in, for example,
/// { "op": "remove", "path": "/a/b/c" }
/// </summary>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, TProp>> path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>("remove", GetPath(path, null), from: null));
return this;
}
/// <summary>
/// Remove value from list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, IList<TProp>>> path, int position)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"remove",
GetPath(path, position.ToString()),
from: null));
return this;
}
/// <summary>
/// Remove value from end of list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, IList<TProp>>> path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"remove",
GetPath(path, "-"),
from: null));
return this;
}
/// <summary>
/// Replace value. Will result in, for example,
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"replace",
GetPath(path, null),
from: null,
value: value));
return this;
}
/// <summary>
/// Replace value in a list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, IList<TProp>>> path,
TProp value, int position)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"replace",
GetPath(path, position.ToString()),
from: null,
value: value));
return this;
}
/// <summary>
/// Replace value at end of a list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"replace",
GetPath(path, "-"),
from: null,
value: value));
return this;
}
/// <summary>
/// Test value. Will result in, for example,
/// { "op": "test", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"test",
GetPath(path, null),
from: null,
value: value));
return this;
}
/// <summary>
/// Test value in a list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, IList<TProp>>> path,
TProp value, int position)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"test",
GetPath(path, position.ToString()),
from: null,
value: value));
return this;
}
/// <summary>
/// Test value at end of a list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"test",
GetPath(path, "-"),
from: null,
value: value));
return this;
}
/// <summary>
/// Removes value at specified location and add it to the target location. Will result in, for example:
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
/// </summary>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, TProp>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, null),
GetPath(from, null)));
return this;
}
/// <summary>
/// Move from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, TProp>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, null),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Move from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <param name="positionTo">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, IList<TProp>>> path,
int positionTo)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, positionTo.ToString()),
GetPath(from, null)));
return this;
}
/// <summary>
/// Move from a position in a list to another location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position (source)</param>
/// <param name="path">target location</param>
/// <param name="positionTo">position (target)</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, IList<TProp>>> path,
int positionTo)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, positionTo.ToString()),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Move from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, IList<TProp>>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, "-"),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Move to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Move<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, IList<TProp>>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"move",
GetPath(path, "-"),
GetPath(from, null)));
return this;
}
/// <summary>
/// Copy the value at specified location to the target location. Willr esult in, for example:
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// </summary>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, TProp>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, null),
GetPath(from, null)));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, TProp>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, null),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Copy from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <param name="positionTo">position</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, IList<TProp>>> path,
int positionTo)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, positionTo.ToString()),
GetPath(from, null)));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position (source)</param>
/// <param name="path">target location</param>
/// <param name="positionTo">position (target)</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, IList<TProp>>> path,
int positionTo)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, positionTo.ToString()),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Copy from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="positionFrom">position</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, IList<TProp>>> from,
int positionFrom,
Expression<Func<TModel, IList<TProp>>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, "-"),
GetPath(from, positionFrom.ToString())));
return this;
}
/// <summary>
/// Copy to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<TModel> Copy<TProp>(
Expression<Func<TModel, TProp>> from,
Expression<Func<TModel, IList<TProp>>> path)
{
if (from == null)
{
throw new ArgumentNullException(nameof(from));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Operations.Add(new Operation<TModel>(
"copy",
GetPath(path, "-"),
GetPath(from, null)));
return this;
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
public void ApplyTo(TModel objectToApplyTo)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null));
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="logErrorAction">Action to log errors</param>
public void ApplyTo(TModel objectToApplyTo, Action<JsonPatchError> logErrorAction)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var adapter = new ObjectAdapter(ContractResolver, logErrorAction);
foreach (var op in Operations)
{
try
{
op.Apply(objectToApplyTo, adapter);
}
catch (JsonPatchException jsonPatchException)
{
var errorReporter = logErrorAction ?? ErrorReporter.Default;
errorReporter(new JsonPatchError(objectToApplyTo, op, jsonPatchException.Message));
// As per JSON Patch spec if an operation results in error, further operations should not be executed.
break;
}
}
}
/// <summary>
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="adapter">IObjectAdapter instance to use when applying</param>
public void ApplyTo(TModel objectToApplyTo, IObjectAdapter adapter)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (adapter == null)
{
throw new ArgumentNullException(nameof(adapter));
}
// apply each operation in order
foreach (var op in Operations)
{
op.Apply(objectToApplyTo, adapter);
}
}
IList<Operation> IJsonPatchDocument.GetOperations()
{
var allOps = new List<Operation>();
if (Operations != null)
{
foreach (var op in Operations)
{
var untypedOp = new Operation
{
op = op.op,
value = op.value,
path = op.path,
from = op.from
};
allOps.Add(untypedOp);
}
}
return allOps;
}
// Internal for testing
internal string GetPath<TProp>(Expression<Func<TModel, TProp>> expr, string position)
{
var segments = GetPathSegments(expr.Body);
var path = String.Join("/", segments);
if (position != null)
{
path += "/" + position;
if (segments.Count == 0)
{
return path;
}
}
return "/" + path;
}
private List<string> GetPathSegments(Expression expr)
{
var listOfSegments = new List<string>();
switch (expr.NodeType)
{
case ExpressionType.ArrayIndex:
var binaryExpression = (BinaryExpression)expr;
listOfSegments.AddRange(GetPathSegments(binaryExpression.Left));
listOfSegments.Add(binaryExpression.Right.ToString());
return listOfSegments;
case ExpressionType.Call:
var methodCallExpression = (MethodCallExpression)expr;
listOfSegments.AddRange(GetPathSegments(methodCallExpression.Object));
listOfSegments.Add(EvaluateExpression(methodCallExpression.Arguments[0]));
return listOfSegments;
case ExpressionType.Convert:
listOfSegments.AddRange(GetPathSegments(((UnaryExpression)expr).Operand));
return listOfSegments;
case ExpressionType.MemberAccess:
var memberExpression = expr as MemberExpression;
listOfSegments.AddRange(GetPathSegments(memberExpression.Expression));
// Get property name, respecting JsonProperty attribute
listOfSegments.Add(GetPropertyNameFromMemberExpression(memberExpression));
return listOfSegments;
case ExpressionType.Parameter:
// Fits "x => x" (the whole document which is "" as JSON pointer)
return listOfSegments;
default:
throw new InvalidOperationException(Resources.FormatExpressionTypeNotSupported(expr));
}
}
private string GetPropertyNameFromMemberExpression(MemberExpression memberExpression)
{
var jsonObjectContract = ContractResolver.ResolveContract(memberExpression.Expression.Type) as JsonObjectContract;
if (jsonObjectContract != null)
{
return jsonObjectContract.Properties
.First(jsonProperty => jsonProperty.UnderlyingName == memberExpression.Member.Name)
.PropertyName;
}
return null;
}
private static bool ContinueWithSubPath(ExpressionType expressionType)
{
return (expressionType == ExpressionType.ArrayIndex
|| expressionType == ExpressionType.Call
|| expressionType == ExpressionType.Convert
|| expressionType == ExpressionType.MemberAccess);
}
// Evaluates the value of the key or index which may be an int or a string,
// or some other expression type.
// The expression is converted to a delegate and the result of executing the delegate is returned as a string.
private static string EvaluateExpression(Expression expression)
{
var converted = Expression.Convert(expression, typeof(object));
var fakeParameter = Expression.Parameter(typeof(object), null);
var lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
var func = lambda.Compile();
return Convert.ToString(func(null), CultureInfo.InvariantCulture);
}
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Operations;
namespace Microsoft.AspNetCore.JsonPatch
{
/// <summary>
/// Captures error message and the related entity and the operation that caused it.
/// </summary>
public class JsonPatchError
{
/// <summary>
/// Initializes a new instance of <see cref="JsonPatchError"/>.
/// </summary>
/// <param name="affectedObject">The object that is affected by the error.</param>
/// <param name="operation">The <see cref="Operation"/> that caused the error.</param>
/// <param name="errorMessage">The error message.</param>
public JsonPatchError(
object affectedObject,
Operation operation,
string errorMessage)
{
if (errorMessage == null)
{
throw new ArgumentNullException(nameof(errorMessage));
}
AffectedObject = affectedObject;
Operation = operation;
ErrorMessage = errorMessage;
}
/// <summary>
/// Gets the object that is affected by the error.
/// </summary>
public object AffectedObject { get; }
/// <summary>
/// Gets the <see cref="Operation"/> that caused the error.
/// </summary>
public Operation Operation { get; }
/// <summary>
/// Gets the error message.
/// </summary>
public string ErrorMessage { get; }
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core support for JSON PATCH.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;json;jsonpatch</PackageTags>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Newtonsoft.Json" />
<Reference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,82 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Operations
{
public class Operation : OperationBase
{
[JsonProperty("value")]
public object value { get; set; }
public Operation()
{
}
public Operation(string op, string path, string from, object value)
: base(op, path, from)
{
this.value = value;
}
public Operation(string op, string path, string from)
: base(op, path, from)
{
}
public void Apply(object objectToApplyTo, IObjectAdapter adapter)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (adapter == null)
{
throw new ArgumentNullException(nameof(adapter));
}
switch (OperationType)
{
case OperationType.Add:
adapter.Add(this, objectToApplyTo);
break;
case OperationType.Remove:
adapter.Remove(this, objectToApplyTo);
break;
case OperationType.Replace:
adapter.Replace(this, objectToApplyTo);
break;
case OperationType.Move:
adapter.Move(this, objectToApplyTo);
break;
case OperationType.Copy:
adapter.Copy(this, objectToApplyTo);
break;
case OperationType.Test:
if (adapter is IObjectAdapterWithTest adapterWithTest)
{
adapterWithTest.Test(this, objectToApplyTo);
break;
}
else
{
throw new NotSupportedException(Resources.TestOperationNotSupported);
}
default:
break;
}
}
public bool ShouldSerializevalue()
{
return (OperationType == OperationType.Add
|| OperationType == OperationType.Replace
|| OperationType == OperationType.Test);
}
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Operations
{
public class OperationBase
{
private string _op;
private OperationType _operationType;
[JsonIgnore]
public OperationType OperationType
{
get
{
return _operationType;
}
}
[JsonProperty("path")]
public string path { get; set; }
[JsonProperty("op")]
public string op
{
get
{
return _op;
}
set
{
OperationType result;
if (!Enum.TryParse(value, ignoreCase: true, result: out result))
{
result = OperationType.Invalid;
}
_operationType = result;
_op = value;
}
}
[JsonProperty("from")]
public string from { get; set; }
public OperationBase()
{
}
public OperationBase(string op, string path, string from)
{
if (op == null)
{
throw new ArgumentNullException(nameof(op));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
this.op = op;
this.path = path;
this.from = from;
}
public bool ShouldSerializefrom()
{
return (OperationType == OperationType.Move
|| OperationType == OperationType.Copy);
}
}
}

View File

@ -0,0 +1,94 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
namespace Microsoft.AspNetCore.JsonPatch.Operations
{
public class Operation<TModel> : Operation where TModel : class
{
public Operation()
{
}
public Operation(string op, string path, string from, object value)
: base(op, path, from)
{
if (op == null)
{
throw new ArgumentNullException(nameof(op));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
this.value = value;
}
public Operation(string op, string path, string from)
: base(op, path, from)
{
if (op == null)
{
throw new ArgumentNullException(nameof(op));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
}
public void Apply(TModel objectToApplyTo, IObjectAdapter adapter)
{
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (adapter == null)
{
throw new ArgumentNullException(nameof(adapter));
}
switch (OperationType)
{
case OperationType.Add:
adapter.Add(this, objectToApplyTo);
break;
case OperationType.Remove:
adapter.Remove(this, objectToApplyTo);
break;
case OperationType.Replace:
adapter.Replace(this, objectToApplyTo);
break;
case OperationType.Move:
adapter.Move(this, objectToApplyTo);
break;
case OperationType.Copy:
adapter.Copy(this, objectToApplyTo);
break;
case OperationType.Test:
if (adapter is IObjectAdapterWithTest adapterWithTest)
{
adapterWithTest.Test(this, objectToApplyTo);
break;
}
else
{
throw new JsonPatchException(new JsonPatchError(objectToApplyTo, this, Resources.TestOperationNotSupported));
}
case OperationType.Invalid:
throw new JsonPatchException(
Resources.FormatInvalidJsonPatchOperation(op), innerException: null);
default:
break;
}
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Operations
{
public enum OperationType
{
Add,
Remove,
Replace,
Move,
Copy,
Test,
Invalid
}
}

View File

@ -0,0 +1,7 @@

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.JsonPatch.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,338 @@
// <auto-generated />
namespace Microsoft.AspNetCore.JsonPatch
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class Resources
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNetCore.JsonPatch.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// The property at '{0}' could not be copied.
/// </summary>
internal static string CannotCopyProperty
{
get => GetString("CannotCopyProperty");
}
/// <summary>
/// The property at '{0}' could not be copied.
/// </summary>
internal static string FormatCannotCopyProperty(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotCopyProperty"), p0);
/// <summary>
/// The type of the property at path '{0}' could not be determined.
/// </summary>
internal static string CannotDeterminePropertyType
{
get => GetString("CannotDeterminePropertyType");
}
/// <summary>
/// The type of the property at path '{0}' could not be determined.
/// </summary>
internal static string FormatCannotDeterminePropertyType(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0);
/// <summary>
/// The '{0}' operation at path '{1}' could not be performed.
/// </summary>
internal static string CannotPerformOperation
{
get => GetString("CannotPerformOperation");
}
/// <summary>
/// The '{0}' operation at path '{1}' could not be performed.
/// </summary>
internal static string FormatCannotPerformOperation(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotPerformOperation"), p0, p1);
/// <summary>
/// The property at '{0}' could not be read.
/// </summary>
internal static string CannotReadProperty
{
get => GetString("CannotReadProperty");
}
/// <summary>
/// The property at '{0}' could not be read.
/// </summary>
internal static string FormatCannotReadProperty(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotReadProperty"), p0);
/// <summary>
/// The property at path '{0}' could not be updated.
/// </summary>
internal static string CannotUpdateProperty
{
get => GetString("CannotUpdateProperty");
}
/// <summary>
/// The property at path '{0}' could not be updated.
/// </summary>
internal static string FormatCannotUpdateProperty(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0);
/// <summary>
/// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.
/// </summary>
internal static string ExpressionTypeNotSupported
{
get => GetString("ExpressionTypeNotSupported");
}
/// <summary>
/// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.
/// </summary>
internal static string FormatExpressionTypeNotSupported(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ExpressionTypeNotSupported"), p0);
/// <summary>
/// The index value provided by path segment '{0}' is out of bounds of the array size.
/// </summary>
internal static string IndexOutOfBounds
{
get => GetString("IndexOutOfBounds");
}
/// <summary>
/// The index value provided by path segment '{0}' is out of bounds of the array size.
/// </summary>
internal static string FormatIndexOutOfBounds(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("IndexOutOfBounds"), p0);
/// <summary>
/// The path segment '{0}' is invalid for an array index.
/// </summary>
internal static string InvalidIndexValue
{
get => GetString("InvalidIndexValue");
}
/// <summary>
/// The path segment '{0}' is invalid for an array index.
/// </summary>
internal static string FormatInvalidIndexValue(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexValue"), p0);
/// <summary>
/// The JSON patch document was malformed and could not be parsed.
/// </summary>
internal static string InvalidJsonPatchDocument
{
get => GetString("InvalidJsonPatchDocument");
}
/// <summary>
/// The JSON patch document was malformed and could not be parsed.
/// </summary>
internal static string FormatInvalidJsonPatchDocument()
=> GetString("InvalidJsonPatchDocument");
/// <summary>
/// Invalid JsonPatch operation '{0}'.
/// </summary>
internal static string InvalidJsonPatchOperation
{
get => GetString("InvalidJsonPatchOperation");
}
/// <summary>
/// Invalid JsonPatch operation '{0}'.
/// </summary>
internal static string FormatInvalidJsonPatchOperation(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidJsonPatchOperation"), p0);
/// <summary>
/// The provided path segment '{0}' cannot be converted to the target type.
/// </summary>
internal static string InvalidPathSegment
{
get => GetString("InvalidPathSegment");
}
/// <summary>
/// The provided path segment '{0}' cannot be converted to the target type.
/// </summary>
internal static string FormatInvalidPathSegment(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidPathSegment"), p0);
/// <summary>
/// The provided string '{0}' is an invalid path.
/// </summary>
internal static string InvalidValueForPath
{
get => GetString("InvalidValueForPath");
}
/// <summary>
/// The provided string '{0}' is an invalid path.
/// </summary>
internal static string FormatInvalidValueForPath(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForPath"), p0);
/// <summary>
/// The value '{0}' is invalid for target location.
/// </summary>
internal static string InvalidValueForProperty
{
get => GetString("InvalidValueForProperty");
}
/// <summary>
/// The value '{0}' is invalid for target location.
/// </summary>
internal static string FormatInvalidValueForProperty(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0);
/// <summary>
/// '{0}' must be of type '{1}'.
/// </summary>
internal static string ParameterMustMatchType
{
get => GetString("ParameterMustMatchType");
}
/// <summary>
/// '{0}' must be of type '{1}'.
/// </summary>
internal static string FormatParameterMustMatchType(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("ParameterMustMatchType"), p0, p1);
/// <summary>
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
/// </summary>
internal static string PatchNotSupportedForArrays
{
get => GetString("PatchNotSupportedForArrays");
}
/// <summary>
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
/// </summary>
internal static string FormatPatchNotSupportedForArrays(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForArrays"), p0);
/// <summary>
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
/// </summary>
internal static string PatchNotSupportedForNonGenericLists
{
get => GetString("PatchNotSupportedForNonGenericLists");
}
/// <summary>
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
/// </summary>
internal static string FormatPatchNotSupportedForNonGenericLists(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForNonGenericLists"), p0);
/// <summary>
/// The target location specified by path segment '{0}' was not found.
/// </summary>
internal static string TargetLocationAtPathSegmentNotFound
{
get => GetString("TargetLocationAtPathSegmentNotFound");
}
/// <summary>
/// The target location specified by path segment '{0}' was not found.
/// </summary>
internal static string FormatTargetLocationAtPathSegmentNotFound(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationAtPathSegmentNotFound"), p0);
/// <summary>
/// For operation '{0}', the target location specified by path '{1}' was not found.
/// </summary>
internal static string TargetLocationNotFound
{
get => GetString("TargetLocationNotFound");
}
/// <summary>
/// For operation '{0}', the target location specified by path '{1}' was not found.
/// </summary>
internal static string FormatTargetLocationNotFound(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationNotFound"), p0, p1);
/// <summary>
/// The test operation is not supported.
/// </summary>
internal static string TestOperationNotSupported
{
get => GetString("TestOperationNotSupported");
}
/// <summary>
/// The test operation is not supported.
/// </summary>
internal static string FormatTestOperationNotSupported()
=> GetString("TestOperationNotSupported");
/// <summary>
/// The current value '{0}' at position '{2}' is not equal to the test value '{1}'.
/// </summary>
internal static string ValueAtListPositionNotEqualToTestValue
{
get => GetString("ValueAtListPositionNotEqualToTestValue");
}
/// <summary>
/// The current value '{0}' at position '{2}' is not equal to the test value '{1}'.
/// </summary>
internal static string FormatValueAtListPositionNotEqualToTestValue(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("ValueAtListPositionNotEqualToTestValue"), p0, p1, p2);
/// <summary>
/// The value at '{0}' cannot be null or empty to perform the test operation.
/// </summary>
internal static string ValueForTargetSegmentCannotBeNullOrEmpty
{
get => GetString("ValueForTargetSegmentCannotBeNullOrEmpty");
}
/// <summary>
/// The value at '{0}' cannot be null or empty to perform the test operation.
/// </summary>
internal static string FormatValueForTargetSegmentCannotBeNullOrEmpty(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ValueForTargetSegmentCannotBeNullOrEmpty"), p0);
/// <summary>
/// The current value '{0}' at path '{2}' is not equal to the test value '{1}'.
/// </summary>
internal static string ValueNotEqualToTestValue
{
get => GetString("ValueNotEqualToTestValue");
}
/// <summary>
/// The current value '{0}' at path '{2}' is not equal to the test value '{1}'.
/// </summary>
internal static string FormatValueNotEqualToTestValue(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("ValueNotEqualToTestValue"), p0, p1, p2);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -0,0 +1,186 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CannotCopyProperty" xml:space="preserve">
<value>The property at '{0}' could not be copied.</value>
</data>
<data name="CannotDeterminePropertyType" xml:space="preserve">
<value>The type of the property at path '{0}' could not be determined.</value>
</data>
<data name="CannotPerformOperation" xml:space="preserve">
<value>The '{0}' operation at path '{1}' could not be performed.</value>
</data>
<data name="CannotReadProperty" xml:space="preserve">
<value>The property at '{0}' could not be read.</value>
</data>
<data name="CannotUpdateProperty" xml:space="preserve">
<value>The property at path '{0}' could not be updated.</value>
</data>
<data name="ExpressionTypeNotSupported" xml:space="preserve">
<value>The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.</value>
</data>
<data name="IndexOutOfBounds" xml:space="preserve">
<value>The index value provided by path segment '{0}' is out of bounds of the array size.</value>
</data>
<data name="InvalidIndexValue" xml:space="preserve">
<value>The path segment '{0}' is invalid for an array index.</value>
</data>
<data name="InvalidJsonPatchDocument" xml:space="preserve">
<value>The JSON patch document was malformed and could not be parsed.</value>
</data>
<data name="InvalidJsonPatchOperation" xml:space="preserve">
<value>Invalid JsonPatch operation '{0}'.</value>
</data>
<data name="InvalidPathSegment" xml:space="preserve">
<value>The provided path segment '{0}' cannot be converted to the target type.</value>
</data>
<data name="InvalidValueForPath" xml:space="preserve">
<value>The provided string '{0}' is an invalid path.</value>
</data>
<data name="InvalidValueForProperty" xml:space="preserve">
<value>The value '{0}' is invalid for target location.</value>
</data>
<data name="ParameterMustMatchType" xml:space="preserve">
<value>'{0}' must be of type '{1}'.</value>
</data>
<data name="PatchNotSupportedForArrays" xml:space="preserve">
<value>The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.</value>
</data>
<data name="PatchNotSupportedForNonGenericLists" xml:space="preserve">
<value>The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.</value>
</data>
<data name="TargetLocationAtPathSegmentNotFound" xml:space="preserve">
<value>The target location specified by path segment '{0}' was not found.</value>
</data>
<data name="TargetLocationNotFound" xml:space="preserve">
<value>For operation '{0}', the target location specified by path '{1}' was not found.</value>
</data>
<data name="TestOperationNotSupported" xml:space="preserve">
<value>The test operation is not supported.</value>
</data>
<data name="ValueAtListPositionNotEqualToTestValue" xml:space="preserve">
<value>The current value '{0}' at position '{2}' is not equal to the test value '{1}'.</value>
</data>
<data name="ValueForTargetSegmentCannotBeNullOrEmpty" xml:space="preserve">
<value>The value at '{0}' cannot be null or empty to perform the test operation.</value>
</data>
<data name="ValueNotEqualToTestValue" xml:space="preserve">
<value>The current value '{0}' at path '{2}' is not equal to the test value '{1}'.</value>
</data>
</root>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,153 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch
{
public class CustomNamingStrategyTests
{
[Fact]
public void AddProperty_ToDynamicTestObject_WithCustomNamingStrategy()
{
// Arrange
var contractResolver = new DefaultContractResolver
{
NamingStrategy = new TestNamingStrategy()
};
dynamic targetObject = new DynamicTestObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Add("NewInt", 1);
patchDocument.ContractResolver = contractResolver;
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(1, targetObject.customNewInt);
Assert.Equal(1, targetObject.Test);
}
[Fact]
public void CopyPropertyValue_ToDynamicTestObject_WithCustomNamingStrategy()
{
// Arrange
var contractResolver = new DefaultContractResolver
{
NamingStrategy = new TestNamingStrategy()
};
dynamic targetObject = new DynamicTestObject();
targetObject.customStringProperty = "A";
targetObject.customAnotherStringProperty = "B";
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("StringProperty", "AnotherStringProperty");
patchDocument.ContractResolver = contractResolver;
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.customAnotherStringProperty);
}
[Fact]
public void MovePropertyValue_ForExpandoObject_WithCustomNamingStrategy()
{
// Arrange
var contractResolver = new DefaultContractResolver
{
NamingStrategy = new TestNamingStrategy()
};
dynamic targetObject = new ExpandoObject();
targetObject.customStringProperty = "A";
targetObject.customAnotherStringProperty = "B";
var patchDocument = new JsonPatchDocument();
patchDocument.Move("StringProperty", "AnotherStringProperty");
patchDocument.ContractResolver = contractResolver;
// Act
patchDocument.ApplyTo(targetObject);
var cont = targetObject as IDictionary<string, object>;
cont.TryGetValue("customStringProperty", out var valueFromDictionary);
// Assert
Assert.Equal("A", targetObject.customAnotherStringProperty);
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveProperty_FromDictionaryObject_WithCustomNamingStrategy()
{
// Arrange
var contractResolver = new DefaultContractResolver
{
NamingStrategy = new TestNamingStrategy()
};
var targetObject = new Dictionary<string, int>()
{
{ "customTest", 1},
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Test");
patchDocument.ContractResolver = contractResolver;
// Act
patchDocument.ApplyTo(targetObject);
var cont = targetObject as IDictionary<string, int>;
cont.TryGetValue("customTest", out var valueFromDictionary);
// Assert
Assert.Equal(0, valueFromDictionary);
}
[Fact]
public void ReplacePropertyValue_ForExpandoObject_WithCustomNamingStrategy()
{
// Arrange
var contractResolver = new DefaultContractResolver
{
NamingStrategy = new TestNamingStrategy()
};
dynamic targetObject = new ExpandoObject();
targetObject.customTest = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("Test", 2);
patchDocument.ContractResolver = contractResolver;
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(2, targetObject.customTest);
}
private class TestNamingStrategy : NamingStrategy
{
public new bool ProcessDictionaryKeys => true;
public override string GetDictionaryKey(string key)
{
return "custom" + key;
}
protected override string ResolvePropertyName(string name)
{
return name;
}
}
}
}

View File

@ -0,0 +1,190 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class AnonymousObjectIntegrationTest
{
[Fact]
public void AddNewProperty_ShouldFail()
{
// Arrange
var targetObject = new { };
var patchDocument = new JsonPatchDocument();
patchDocument.Add("NewProperty", 4);
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'NewProperty' was not found.",
exception.Message);
}
[Fact]
public void AddNewProperty_ToNestedAnonymousObject_ShouldFail()
{
// Arrange
dynamic targetObject = new
{
Test = 1,
nested = new { }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Add("Nested/NewInt", 1);
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'NewInt' was not found.",
exception.Message);
}
[Fact]
public void AddDoesNotReplace()
{
// Arrange
var targetObject = new
{
StringProperty = "A"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Add("StringProperty", "B");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The property at path 'StringProperty' could not be updated.",
exception.Message);
}
[Fact]
public void RemoveProperty_ShouldFail()
{
// Arrange
dynamic targetObject = new
{
Test = 1
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Test");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The property at path 'Test' could not be updated.",
exception.Message);
}
[Fact]
public void ReplaceProperty_ShouldFail()
{
// Arrange
var targetObject = new
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("StringProperty", "AnotherStringProperty");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The property at path 'StringProperty' could not be updated.",
exception.Message);
}
[Fact]
public void MoveProperty_ShouldFail()
{
// Arrange
var targetObject = new
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Move("StringProperty", "AnotherStringProperty");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The property at path 'StringProperty' could not be updated.",
exception.Message);
}
[Fact]
public void TestStringProperty_IsSucessful()
{
// Arrange
var targetObject = new
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Test("StringProperty", "A");
// Act & Assert
patchDocument.ApplyTo(targetObject);
}
[Fact]
public void TestStringProperty_Fails()
{
// Arrange
var targetObject = new
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Test("StringProperty", "B");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The current value 'A' at path 'StringProperty' is not equal to the test value 'B'.",
exception.Message);
}
}
}

View File

@ -0,0 +1,319 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class DictionaryTest
{
[Fact]
public void TestIntegerValue_IsSuccessful()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Test("/DictionaryOfStringToInteger/two", 2);
// Act & Assert
patchDocument.ApplyTo(model);
}
[Fact]
public void AddIntegerValue_Succeeds()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Add("/DictionaryOfStringToInteger/three", 3);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(3, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
Assert.Equal(2, model.DictionaryOfStringToInteger["two"]);
Assert.Equal(3, model.DictionaryOfStringToInteger["three"]);
}
[Fact]
public void RemoveIntegerValue_Succeeds()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("/DictionaryOfStringToInteger/two");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(1, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
}
[Fact]
public void MoveIntegerValue_Succeeds()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Move("/DictionaryOfStringToInteger/one", "/DictionaryOfStringToInteger/two");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(1, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["two"]);
}
[Fact]
public void ReplaceIntegerValue_Succeeds()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("/DictionaryOfStringToInteger/two", 20);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
Assert.Equal(20, model.DictionaryOfStringToInteger["two"]);
}
[Fact]
public void CopyIntegerValue_Succeeds()
{
// Arrange
var model = new IntDictionary();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("/DictionaryOfStringToInteger/one", "/DictionaryOfStringToInteger/two");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
Assert.Equal(1, model.DictionaryOfStringToInteger["two"]);
}
private class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
}
private class Address
{
public string City { get; set; }
}
private class IntDictionary
{
public IDictionary<string, int> DictionaryOfStringToInteger { get; } = new Dictionary<string, int>();
}
private class CustomerDictionary
{
public IDictionary<int, Customer> DictionaryOfStringToCustomer { get; } = new Dictionary<int, Customer>();
}
[Fact]
public void TestPocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "James" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
var patchDocument = new JsonPatchDocument();
patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
// Act & Assert
patchDocument.ApplyTo(model);
}
[Fact]
public void TestPocoObject_FailsWhenTestValueIsNotEqualToObjectValue()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "James" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
var patchDocument = new JsonPatchDocument();
patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "Mike");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(model);
});
// Assert
Assert.Equal("The current value 'James' at path 'Name' is not equal to the test value 'Mike'.", exception.Message);
}
[Fact]
public void AddReplacesPocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "Jamesss" };
var key2 = 200;
var value2 = new Customer() { Name = "Mike" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Add($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
}
[Fact]
public void RemovePocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "Jamesss" };
var key2 = 200;
var value2 = new Customer() { Name = "Mike" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove($"/DictionaryOfStringToCustomer/{key1}/Name");
// Act
patchDocument.ApplyTo(model);
// Assert
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.Null(actualValue1.Name);
}
[Fact]
public void MovePocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "James" };
var key2 = 200;
var value2 = new Customer() { Name = "Mike" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Move($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name");
// Act
patchDocument.ApplyTo(model);
// Assert
var actualValue2 = model.DictionaryOfStringToCustomer[key2];
Assert.NotNull(actualValue2);
Assert.Equal("James", actualValue2.Name);
}
[Fact]
public void CopyPocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "James" };
var key2 = 200;
var value2 = new Customer() { Name = "Mike" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Copy($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue2 = model.DictionaryOfStringToCustomer[key2];
Assert.NotNull(actualValue2);
Assert.Equal("James", actualValue2.Name);
}
[Fact]
public void ReplacePocoObject_Succeeds()
{
// Arrange
var key1 = 100;
var value1 = new Customer() { Name = "Jamesss" };
var key2 = 200;
var value2 = new Customer() { Name = "Mike" };
var model = new CustomerDictionary();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
}
[Fact]
public void ReplacePocoObject_WithEscaping_Succeeds()
{
// Arrange
var key1 = "Foo/Name";
var value1 = 100;
var key2 = "Foo";
var value2 = 200;
var model = new IntDictionary();
model.DictionaryOfStringToInteger[key1] = value1;
model.DictionaryOfStringToInteger[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToInteger/Foo~1Name", 300);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToInteger.Count);
var actualValue1 = model.DictionaryOfStringToInteger[key1];
var actualValue2 = model.DictionaryOfStringToInteger[key2];
Assert.Equal(300, actualValue1);
Assert.Equal(200, actualValue2);
}
}
}

View File

@ -0,0 +1,236 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class DynamicObjectIntegrationTest
{
[Fact]
public void AddResults_ShouldReplaceExistingPropertyValue_InNestedDynamicObject()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.Nested = new NestedObject();
dynamicTestObject.Nested.DynamicProperty = new DynamicTestObject();
dynamicTestObject.Nested.DynamicProperty.InBetweenFirst = new DynamicTestObject();
dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond = new DynamicTestObject();
dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Add("/Nested/DynamicProperty/InBetweenFirst/InBetweenSecond/StringProperty", "B");
// Act
patchDocument.ApplyTo(dynamicTestObject);
// Assert
Assert.Equal("B", dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty);
}
[Fact]
public void ShouldNotBeAbleToAdd_ToNonExistingProperty_ThatIsNotTheRoot()
{
//Adding to a Nonexistent Target
//
// An example target JSON document:
// { "foo": "bar" }
// A JSON Patch document:
// [
// { "op": "add", "path": "/baz/bat", "value": "qux" }
// ]
// This JSON Patch document, applied to the target JSON document above,
// would result in an error (therefore, it would not be applied),
// because the "add" operation's target location that references neither
// the root of the document, nor a member of an existing object, nor a
// member of an existing array.
// Arrange
var nestedObject = new NestedObject()
{
DynamicProperty = new DynamicTestObject()
};
var patchDocument = new JsonPatchDocument();
patchDocument.Add("DynamicProperty/OtherProperty/IntProperty", 1);
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(nestedObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'OtherProperty' was not found.", exception.Message);
}
[Fact]
public void CopyProperties_InNestedDynamicObject()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.NestedDynamicObject = new DynamicTestObject();
dynamicTestObject.NestedDynamicObject.StringProperty = "A";
dynamicTestObject.NestedDynamicObject.AnotherStringProperty = "B";
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("NestedDynamicObject/StringProperty", "NestedDynamicObject/AnotherStringProperty");
// Act
patchDocument.ApplyTo(dynamicTestObject);
// Assert
Assert.Equal("A", dynamicTestObject.NestedDynamicObject.AnotherStringProperty);
}
[Fact]
public void MoveToNonExistingProperty_InDynamicObject_ShouldAddNewProperty()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Move("StringProperty", "AnotherStringProperty");
// Act
patchDocument.ApplyTo(dynamicTestObject);
dynamicTestObject.TryGetValue("StringProperty", out object valueFromDictionary);
// Assert
Assert.Equal("A", dynamicTestObject.AnotherStringProperty);
Assert.Null(valueFromDictionary);
}
[Fact]
public void MovePropertyValue_FromDynamicObject_ToTypedObject()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.StringProperty = "A";
dynamicTestObject.SimpleObject = new SimpleObject() { AnotherStringProperty = "B" };
var patchDocument = new JsonPatchDocument();
patchDocument.Move("StringProperty", "SimpleObject/AnotherStringProperty");
// Act
patchDocument.ApplyTo(dynamicTestObject);
dynamicTestObject.TryGetValue("StringProperty", out object valueFromDictionary);
// Assert
Assert.Equal("A", dynamicTestObject.SimpleObject.AnotherStringProperty);
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveNestedProperty_FromDynamicObject()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.Test = new DynamicTestObject();
dynamicTestObject.Test.AnotherTest = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Test");
// Act
patchDocument.ApplyTo(dynamicTestObject);
dynamicTestObject.TryGetValue("Test", out object valueFromDictionary);
// Assert
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveFromNestedObject_InDynamicObject_MixedCase_ThrowsPathNotFoundException()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.SimpleObject = new SimpleObject()
{
StringProperty = "A"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Simpleobject/stringProperty");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(dynamicTestObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'Simpleobject' was not found.", exception.Message);
}
[Fact]
public void ReplaceNestedTypedObject_InDynamicObject()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.SimpleObject = new SimpleObject()
{
IntegerValue = 5,
IntegerList = new List<int>() { 1, 2, 3 }
};
var newObject = new SimpleObject()
{
DoubleValue = 1
};
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("SimpleObject", newObject);
// Act
patchDocument.ApplyTo(dynamicTestObject);
// Assert
Assert.Equal(1, dynamicTestObject.SimpleObject.DoubleValue);
Assert.Equal(0, dynamicTestObject.SimpleObject.IntegerValue);
Assert.Null(dynamicTestObject.SimpleObject.IntegerList);
}
[Fact]
public void TestStringPropertyValue_IsSuccessful()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.Property = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Test("Property", "A");
// Act & Assert
patchDocument.ApplyTo(dynamicTestObject);
}
[Fact]
public void TestIntegerPropertyValue_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
dynamic dynamicTestObject = new DynamicTestObject();
dynamicTestObject.Nested = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Test("Nested/IntegerList/0", 2);
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(dynamicTestObject);
});
// Assert
Assert.Equal("The current value '1' at position '0' is not equal to the test value '2'.", exception.Message);
}
}
}

View File

@ -0,0 +1,327 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Dynamic;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class ExpandoObjectIntegrationTest
{
[Fact]
public void AddNewIntProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Add("NewInt", 1);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(1, targetObject.NewInt);
Assert.Equal(1, targetObject.Test);
}
[Fact]
public void AddNewProperty_ToTypedObject_InExpandoObject()
{
// Arrange
dynamic dynamicProperty = new ExpandoObject();
dynamicProperty.StringProperty = "A";
var targetObject = new NestedObject()
{
DynamicProperty = dynamicProperty
};
var patchDocument = new JsonPatchDocument();
patchDocument.Add("DynamicProperty/StringProperty", "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.DynamicProperty.StringProperty);
}
[Fact]
public void AddReplaces_ExistingProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Add("StringProperty", "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.StringProperty);
}
[Fact]
public void AddReplaces_ExistingProperty_InNestedExpandoObject()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.InBetweenFirst = new ExpandoObject();
targetObject.InBetweenFirst.InBetweenSecond = new ExpandoObject();
targetObject.InBetweenFirst.InBetweenSecond.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Add("/InBetweenFirst/InBetweenSecond/StringProperty", "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.InBetweenFirst.InBetweenSecond.StringProperty);
}
[Fact]
public void ShouldNotReplaceProperty_WithDifferentCase()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Add("stringproperty", "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.StringProperty);
Assert.Equal("B", targetObject.stringproperty);
}
[Fact]
public void TestIntegerProperty_IsSucessful()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Test("Test", 1);
// Act & Assert
patchDocument.ApplyTo(targetObject);
}
[Fact]
public void TestStringProperty_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = "Value";
var patchDocument = new JsonPatchDocument();
patchDocument.Test("Test", "TestValue");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The current value 'Value' at path 'Test' is not equal to the test value 'TestValue'.",
exception.Message);
}
[Fact]
public void CopyStringProperty_ToAnotherStringProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.StringProperty = "A";
targetObject.AnotherStringProperty = "B";
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("StringProperty", "AnotherStringProperty");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.AnotherStringProperty);
}
[Fact]
public void MoveIntegerValue_ToAnotherIntegerProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.IntegerValue = 100;
targetObject.AnotherIntegerValue = 200;
var patchDocument = new JsonPatchDocument();
patchDocument.Move("IntegerValue", "AnotherIntegerValue");
// Act
patchDocument.ApplyTo(targetObject);
Assert.Equal(100, targetObject.AnotherIntegerValue);
var cont = targetObject as IDictionary<string, object>;
cont.TryGetValue("IntegerValue", out object valueFromDictionary);
// Assert
Assert.Null(valueFromDictionary);
}
[Fact]
public void Move_ToNonExistingProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.StringProperty = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Move("StringProperty", "AnotherStringProperty");
// Act
patchDocument.ApplyTo(targetObject);
Assert.Equal("A", targetObject.AnotherStringProperty);
var cont = targetObject as IDictionary<string, object>;
cont.TryGetValue("StringProperty", out var valueFromDictionary);
// Assert
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveProperty_ShouldFail_IfItDoesntExist()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("NonExisting");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'NonExisting' was not found.", exception.Message);
}
[Fact]
public void RemoveStringProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Test");
// Act
patchDocument.ApplyTo(targetObject);
var cont = targetObject as IDictionary<string, object>;
cont.TryGetValue("Test", out object valueFromDictionary);
// Assert
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveProperty_MixedCase_ThrowsPathNotFoundException()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = 1;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("test");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'test' was not found.", exception.Message);
}
[Fact]
public void RemoveNestedProperty()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = new ExpandoObject();
targetObject.Test.AnotherTest = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("Test");
// Act
patchDocument.ApplyTo(targetObject);
var cont = targetObject as IDictionary<string, object>;
cont.TryGetValue("Test", out object valueFromDictionary);
// Assert
Assert.Null(valueFromDictionary);
}
[Fact]
public void RemoveNestedProperty_MixedCase_ThrowsPathNotFoundException()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.Test = new ExpandoObject();
targetObject.Test.AnotherTest = "A";
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("test");
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal("The target location specified by path segment 'test' was not found.", exception.Message);
}
[Fact]
public void ReplaceGuid()
{
// Arrange
dynamic targetObject = new ExpandoObject();
targetObject.GuidValue = Guid.NewGuid();
var newGuid = Guid.NewGuid();
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("GuidValue", newGuid);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(newGuid, targetObject.GuidValue);
}
}
}

View File

@ -0,0 +1,366 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class ListIntegrationTest
{
[Fact]
public void TestInList_IsSuccessful()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Test(o => o.SimpleObject.IntegerList, 3, 2);
// Act & Assert
patchDocument.ApplyTo(targetObject);
}
[Fact]
public void TestInList_InvalidPosition()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Test(o => o.SimpleObject.IntegerList, 4, -1);
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDocument.ApplyTo(targetObject); });
Assert.Equal("The index value provided by path segment '-1' is out of bounds of the array size.",
exception.Message);
}
[Fact]
public void AddToIntegerIList()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Add(o => (List<int>)o.SimpleObject.IntegerIList, 4, 0);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, targetObject.SimpleObject.IntegerIList);
}
[Fact]
public void AddToComplextTypeList_SpecifyIndex()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObjectList = new List<SimpleObject>()
{
new SimpleObject
{
StringProperty = "String1"
},
new SimpleObject
{
StringProperty = "String2"
}
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Add(o => o.SimpleObjectList[0].StringProperty, "ChangedString1");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("ChangedString1", targetObject.SimpleObjectList[0].StringProperty);
}
[Fact]
public void AddToListAppend()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Add(o => o.SimpleObject.IntegerList, 4);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, targetObject.SimpleObject.IntegerList);
}
[Fact]
public void RemoveFromList()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("IntegerList/2");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 1, 2 }, targetObject.IntegerList);
}
[Theory]
[InlineData("3")]
[InlineData("-1")]
public void RemoveFromList_InvalidPosition(string position)
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("IntegerList/" + position);
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.ApplyTo(targetObject);
});
// Assert
Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", exception.Message);
}
[Fact]
public void Remove_FromEndOfList()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Remove<int>(o => o.SimpleObject.IntegerList);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 1, 2 }, targetObject.SimpleObject.IntegerList);
}
[Fact]
public void ReplaceFullList_WithCollection()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("IntegerList", new Collection<int>() { 4, 5, 6 });
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 4, 5, 6 }, targetObject.IntegerList);
}
[Fact]
public void Replace_AtEndOfList()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Replace(o => o.SimpleObject.IntegerList, 5);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 1, 2, 5 }, targetObject.SimpleObject.IntegerList);
}
[Fact]
public void Replace_InList_InvalidPosition()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Replace(o => o.SimpleObject.IntegerList, 5, -1);
// Act
var exception = Assert.Throws<JsonPatchException>(() => { patchDocument.ApplyTo(targetObject); });
// Assert
Assert.Equal("The index value provided by path segment '-1' is out of bounds of the array size.", exception.Message);
}
[Fact]
public void CopyFromListToEndOfList()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("IntegerList/0", "IntegerList/-");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 1 }, targetObject.IntegerList);
}
[Fact]
public void CopyFromListToNonList()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("IntegerList/0", "IntegerValue");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(1, targetObject.IntegerValue);
}
[Fact]
public void MoveToEndOfList()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerValue = 5,
IntegerList = new List<int>() { 1, 2, 3 }
};
var patchDocument = new JsonPatchDocument();
patchDocument.Move("IntegerValue", "IntegerList/-");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(0, targetObject.IntegerValue);
Assert.Equal(new List<int>() { 1, 2, 3, 5 }, targetObject.IntegerList);
}
[Fact]
public void Move_KeepsObjectReferenceInList()
{
// Arrange
var simpleObject1 = new SimpleObject() { IntegerValue = 1 };
var simpleObject2 = new SimpleObject() { IntegerValue = 2 };
var simpleObject3 = new SimpleObject() { IntegerValue = 3 };
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObjectList = new List<SimpleObject>() {
simpleObject1,
simpleObject2,
simpleObject3
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Move(o => o.SimpleObjectList, 0, o => o.SimpleObjectList, 1);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<SimpleObject>() { simpleObject2, simpleObject1, simpleObject3 }, targetObject.SimpleObjectList);
Assert.Equal(2, targetObject.SimpleObjectList[0].IntegerValue);
Assert.Equal(1, targetObject.SimpleObjectList[1].IntegerValue);
Assert.Same(simpleObject2, targetObject.SimpleObjectList[0]);
Assert.Same(simpleObject1, targetObject.SimpleObjectList[1]);
}
[Fact]
public void MoveFromList_ToNonList_BetweenHierarchy()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerList = new List<int>() { 1, 2, 3 }
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Move(o => o.SimpleObject.IntegerList, 0, o => o.IntegerValue);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(new List<int>() { 2, 3 }, targetObject.SimpleObject.IntegerList);
Assert.Equal(1, targetObject.IntegerValue);
}
}
}

View File

@ -0,0 +1,342 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Dynamic;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class NestedObjectIntegrationTest
{
[Fact]
public void Replace_DTOWithNullCheck()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObjectWithNullCheck()
{
SimpleObjectWithNullCheck = new SimpleObjectWithNullCheck()
{
StringProperty = "A"
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObjectWithNullCheck>();
patchDocument.Replace(o => o.SimpleObjectWithNullCheck.StringProperty, "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.SimpleObjectWithNullCheck.StringProperty);
}
[Fact]
public void ReplaceNestedObject_WithSerialization()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
IntegerValue = 1
};
var newNested = new NestedObject() { StringProperty = "B" };
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Replace(o => o.NestedObject, newNested);
var serialized = JsonConvert.SerializeObject(patchDocument);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObjectWithNestedObject>>(serialized);
// Act
deserialized.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.NestedObject.StringProperty);
}
[Fact]
public void TestStringProperty_InNestedObject()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
NestedObject = new NestedObject() { StringProperty = "A"}
};
var patchDocument = new JsonPatchDocument<NestedObject>();
patchDocument.Test(o => o.StringProperty, "A");
// Act
patchDocument.ApplyTo(targetObject.NestedObject);
// Assert
Assert.Equal("A", targetObject.NestedObject.StringProperty);
}
[Fact]
public void TestNestedObject()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
NestedObject = new NestedObject() { StringProperty = "B"}
};
var testNested = new NestedObject() { StringProperty = "B" };
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Test(o => o.NestedObject, testNested);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.NestedObject.StringProperty);
}
[Fact]
public void AddReplaces_ExistingStringProperty()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
StringProperty = "A"
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Add(o => o.SimpleObject.StringProperty, "B");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("B", targetObject.SimpleObject.StringProperty);
}
[Fact]
public void AddNewProperty_ToExpandoOject_InTypedObject()
{
var targetObject = new NestedObject()
{
DynamicProperty = new ExpandoObject()
};
var patchDocument = new JsonPatchDocument();
patchDocument.Add("DynamicProperty/NewInt", 1);
patchDocument.ApplyTo(targetObject);
Assert.Equal(1, targetObject.DynamicProperty.NewInt);
}
[Fact]
public void RemoveStringProperty()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
StringProperty = "A"
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Remove(o => o.SimpleObject.StringProperty);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Null(targetObject.SimpleObject.StringProperty);
}
[Fact]
public void CopyStringProperty_ToAnotherStringProperty()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Copy(o => o.SimpleObject.StringProperty, o => o.SimpleObject.AnotherStringProperty);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.SimpleObject.AnotherStringProperty);
}
[Fact]
public void Copy_DeepClonesObject()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
},
InheritedObject = new InheritedObject()
{
StringProperty = "C",
AnotherStringProperty = "D"
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("C", targetObject.SimpleObject.StringProperty);
Assert.Equal("D", targetObject.SimpleObject.AnotherStringProperty);
Assert.Equal("C", targetObject.InheritedObject.StringProperty);
Assert.Equal("D", targetObject.InheritedObject.AnotherStringProperty);
Assert.NotSame(targetObject.SimpleObject.StringProperty, targetObject.InheritedObject.StringProperty);
}
[Fact]
public void Copy_KeepsObjectType()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject(),
InheritedObject = new InheritedObject()
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(typeof(InheritedObject), targetObject.SimpleObject.GetType());
}
[Fact]
public void Copy_BreaksObjectReference()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject(),
InheritedObject = new InheritedObject()
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.NotSame(targetObject.SimpleObject, targetObject.InheritedObject);
}
[Fact]
public void MoveIntegerValue_ToAnotherIntegerProperty()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = new SimpleObject()
{
IntegerValue = 2,
AnotherIntegerValue = 3
}
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Move(o => o.SimpleObject.IntegerValue, o => o.SimpleObject.AnotherIntegerValue);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(2, targetObject.SimpleObject.AnotherIntegerValue);
Assert.Equal(0, targetObject.SimpleObject.IntegerValue);
}
[Fact]
public void Move_KeepsObjectReference()
{
// Arrange
var sDto = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var iDto = new InheritedObject()
{
StringProperty = "C",
AnotherStringProperty = "D"
};
var targetObject = new SimpleObjectWithNestedObject()
{
SimpleObject = sDto,
InheritedObject = iDto
};
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Move(o => o.InheritedObject, o => o.SimpleObject);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("C", targetObject.SimpleObject.StringProperty);
Assert.Equal("D", targetObject.SimpleObject.AnotherStringProperty);
Assert.Same(iDto, targetObject.SimpleObject);
Assert.Null(targetObject.InheritedObject);
}
private class SimpleObjectWithNullCheck
{
private string stringProperty;
public string StringProperty
{
get
{
return stringProperty;
}
set
{
if (value == null)
{
throw new ArgumentNullException();
}
stringProperty = value;
}
}
}
private class SimpleObjectWithNestedObjectWithNullCheck
{
public SimpleObjectWithNullCheck SimpleObjectWithNullCheck { get; set; }
public SimpleObjectWithNestedObjectWithNullCheck()
{
SimpleObjectWithNullCheck = new SimpleObjectWithNullCheck();
}
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests
{
public class SimpleObjectIntegrationTest
{
[Fact]
public void TestDoubleValueProperty()
{
// Arrange
var targetObject = new SimpleObject()
{
DoubleValue = 9.8
};
var patchDocument = new JsonPatchDocument();
patchDocument.Test("DoubleValue", 9.8);
// Act & Assert
patchDocument.ApplyTo(targetObject);
}
[Fact]
public void CopyStringProperty_ToAnotherStringProperty()
{
// Arrange
var targetObject = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("StringProperty", "AnotherStringProperty");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.AnotherStringProperty);
}
[Fact]
public void MoveIntegerProperty_ToAnotherIntegerProperty()
{
// Arrange
var targetObject = new SimpleObject()
{
IntegerValue = 2,
AnotherIntegerValue = 3
};
var patchDocument = new JsonPatchDocument();
patchDocument.Move("IntegerValue", "AnotherIntegerValue");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(2, targetObject.AnotherIntegerValue);
Assert.Equal(0, targetObject.IntegerValue);
}
[Fact]
public void RemoveDecimalPropertyValue()
{
// Arrange
var targetObject = new SimpleObject()
{
DecimalValue = 9.8M
};
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("DecimalValue");
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(0, targetObject.DecimalValue);
}
[Fact]
public void ReplaceGuid()
{
// Arrange
var targetObject = new SimpleObject()
{
GuidValue = Guid.NewGuid()
};
var newGuid = Guid.NewGuid();
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("GuidValue", newGuid);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(newGuid, targetObject.GuidValue);
}
[Fact]
public void AddReplacesGuid()
{
// Arrange
var targetObject = new SimpleObject()
{
GuidValue = Guid.NewGuid()
};
var newGuid = Guid.NewGuid();
var patchDocument = new JsonPatchDocument();
patchDocument.Add("GuidValue", newGuid);
// Act
patchDocument.ApplyTo(targetObject);
// Assert
Assert.Equal(newGuid, targetObject.GuidValue);
}
}
}

View File

@ -0,0 +1,311 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DictionaryAdapterTest
{
[Fact]
public void Add_KeyWhichAlreadyExists_ReplacesExistingValue()
{
// Arrange
var key = "Status";
var dictionary = new Dictionary<string, int>(StringComparer.Ordinal);
dictionary[key] = 404;
var dictionaryAdapter = new DictionaryAdapter<string, int>();
var resolver = new DefaultContractResolver();
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, key, resolver, 200, out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal(200, dictionary[key]);
}
[Fact]
public void Add_IntKeyWhichAlreadyExists_ReplacesExistingValue()
{
// Arrange
var intKey = 1;
var dictionary = new Dictionary<int, object>();
dictionary[intKey] = "Mike";
var dictionaryAdapter = new DictionaryAdapter<int, object>();
var resolver = new DefaultContractResolver();
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, intKey.ToString(), resolver, "James", out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[intKey]);
}
[Fact]
public void GetInvalidKey_ThrowsInvalidPathSegmentException()
{
// Arrange
var dictionaryAdapter = new DictionaryAdapter<int, object>();
var resolver = new DefaultContractResolver();
var key = 1;
var dictionary = new Dictionary<int, object>();
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, key.ToString(), resolver, "James", out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[key]);
// Act
var guidKey = new Guid();
var getStatus = dictionaryAdapter.TryGet(dictionary, guidKey.ToString(), resolver, out var outValue, out message);
// Assert
Assert.False(getStatus);
Assert.Equal($"The provided path segment '{guidKey.ToString()}' cannot be converted to the target type.", message);
Assert.Null(outValue);
}
[Fact]
public void Get_UsingCaseSensitiveKey_FailureScenario()
{
// Arrange
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver, "James", out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[nameKey]);
// Act
var getStatus = dictionaryAdapter.TryGet(dictionary, nameKey.ToUpper(), resolver, out var outValue, out message);
// Assert
Assert.False(getStatus);
Assert.Equal("The target location specified by path segment 'NAME' was not found.", message);
Assert.Null(outValue);
}
[Fact]
public void Get_UsingCaseSensitiveKey_SuccessScenario()
{
// Arrange
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver, "James", out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[nameKey]);
// Act
addStatus = dictionaryAdapter.TryGet(dictionary, nameKey, resolver, out var outValue, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal("James", outValue?.ToString());
}
[Fact]
public void ReplacingExistingItem()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
dictionary.Add(nameKey, "Mike");
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver, "James", out var message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[nameKey]);
}
[Fact]
public void ReplacingExistingItem_WithGuidKey()
{
// Arrange
var guidKey = new Guid();
var dictionary = new Dictionary<Guid, object>();
dictionary.Add(guidKey, "Mike");
var dictionaryAdapter = new DictionaryAdapter<Guid, object>();
var resolver = new DefaultContractResolver();
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, guidKey.ToString(), resolver, "James", out var message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Single(dictionary);
Assert.Equal("James", dictionary[guidKey]);
}
[Fact]
public void ReplacingWithInvalidValue_ThrowsInvalidValueForPropertyException()
{
// Arrange
var guidKey = new Guid();
var dictionary = new Dictionary<Guid, int>();
dictionary.Add(guidKey, 5);
var dictionaryAdapter = new DictionaryAdapter<Guid, int>();
var resolver = new DefaultContractResolver();
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, guidKey.ToString(), resolver, "test", out var message);
// Assert
Assert.False(replaceStatus);
Assert.Equal("The value 'test' is invalid for target location.", message);
Assert.Equal(5, dictionary[guidKey]);
}
[Fact]
public void Replace_NonExistingKey_Fails()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver, "Mike", out var message);
// Assert
Assert.False(replaceStatus);
Assert.Equal("The target location specified by path segment 'Name' was not found.", message);
Assert.Empty(dictionary);
}
[Fact]
public void Remove_NonExistingKey_Fails()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
// Act
var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver, out var message);
// Assert
Assert.False(removeStatus);
Assert.Equal("The target location specified by path segment 'Name' was not found.", message);
Assert.Empty(dictionary);
}
[Fact]
public void Remove_RemovesFromDictionary()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
dictionary[nameKey] = "James";
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
// Act
var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver, out var message);
//Assert
Assert.True(removeStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Empty(dictionary);
}
[Fact]
public void Remove_RemovesFromDictionary_WithUriKey()
{
// Arrange
var uriKey = new Uri("http://www.test.com/name");
var dictionary = new Dictionary<Uri, object>();
dictionary[uriKey] = "James";
var dictionaryAdapter = new DictionaryAdapter<Uri, object>();
var resolver = new DefaultContractResolver();
// Act
var removeStatus = dictionaryAdapter.TryRemove(dictionary, uriKey.ToString(), resolver, out var message);
//Assert
Assert.True(removeStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Empty(dictionary);
}
[Fact]
public void Test_DoesNotThrowException_IfTestIsSuccessful()
{
// Arrange
var key = "Name";
var dictionary = new Dictionary<string, List<object>>();
var value = new List<object>()
{
"James",
2,
new Customer("James", 25)
};
dictionary[key] = value;
var dictionaryAdapter = new DictionaryAdapter<string, List<object>>();
var resolver = new DefaultContractResolver();
// Act
var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, value, out var message);
//Assert
Assert.True(testStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
}
[Fact]
public void Test_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
var key = "Name";
var dictionary = new Dictionary<string, object>();
dictionary[key] = "James";
var dictionaryAdapter = new DictionaryAdapter<string, object>();
var resolver = new DefaultContractResolver();
var expectedErrorMessage = "The current value 'James' at path 'Name' is not equal to the test value 'John'.";
// Act
var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, "John", out var errorMessage);
//Assert
Assert.False(testStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
}
}

View File

@ -0,0 +1,274 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DynamicObjectAdapterTest
{
[Fact]
public void TryAdd_AddsNewProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryAdd(target, segment, resolver, "new", out string errorMessage);
// Assert
Assert.True(status);
Assert.Null(errorMessage);
Assert.Equal("new", target.NewProperty);
}
[Fact]
public void TryAdd_ReplacesExistingPropertyValue()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.List = new List<int>() { 1, 2, 3 };
var value = new List<string>() { "stringValue1", "stringValue2" };
var segment = "List";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryAdd(target, segment, resolver, value, out string errorMessage);
// Assert
Assert.True(status);
Assert.Null(errorMessage);
Assert.Equal(value, target.List);
}
[Fact]
public void TryGet_GetsPropertyValue_ForExistingProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act 1
var addStatus = adapter.TryAdd(target, segment, resolver, "new", out string errorMessage);
// Assert 1
Assert.True(addStatus);
Assert.Null(errorMessage);
Assert.Equal("new", target.NewProperty);
// Act 2
var getStatus = adapter.TryGet(target, segment, resolver, out object getValue, out string getErrorMessage);
// Assert 2
Assert.True(getStatus);
Assert.Null(getErrorMessage);
Assert.Equal(getValue, target.NewProperty);
}
[Fact]
public void TryGet_ThrowsPathNotFoundException_ForNonExistingProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var getStatus = adapter.TryGet(target, segment, resolver, out object getValue, out string getErrorMessage);
// Assert
Assert.False(getStatus);
Assert.Null(getValue);
Assert.Equal($"The target location specified by path segment '{segment}' was not found.", getErrorMessage);
}
[Fact]
public void TryTraverse_FindsNextTarget()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.NestedObject = new DynamicTestObject();
target.NestedObject.NewProperty = "A";
var segment = "NestedObject";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryTraverse(target, segment, resolver, out object nextTarget, out string errorMessage);
// Assert
Assert.True(status);
Assert.Null(errorMessage);
Assert.Equal(target.NestedObject, nextTarget);
}
[Fact]
public void TryTraverse_ThrowsPathNotFoundException_ForNonExistingProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.NestedObject = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryTraverse(target.NestedObject, segment, resolver, out object nextTarget, out string errorMessage);
// Assert
Assert.False(status);
Assert.Equal($"The target location specified by path segment '{segment}' was not found.", errorMessage);
}
[Fact]
public void TryReplace_ReplacesPropertyValue()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.NewProperty = new object();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryReplace(target, segment, resolver, "new", out string errorMessage);
// Assert
Assert.True(status);
Assert.Null(errorMessage);
Assert.Equal("new", target.NewProperty);
}
[Fact]
public void TryReplace_ThrowsPathNotFoundException_ForNonExistingProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryReplace(target, segment, resolver, "test", out string errorMessage);
// Assert
Assert.False(status);
Assert.Equal($"The target location specified by path segment '{segment}' was not found.", errorMessage);
}
[Fact]
public void TryReplace_ThrowsPropertyInvalidException_IfNewValueIsNotTheSameTypeAsInitialValue()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.NewProperty = 1;
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var status = adapter.TryReplace(target, segment, resolver, "test", out string errorMessage);
// Assert
Assert.False(status);
Assert.Equal($"The value 'test' is invalid for target location.", errorMessage);
}
[Theory]
[InlineData(1, 0)]
[InlineData("new", null)]
public void TryRemove_SetsPropertyToDefaultOrNull(object value, object expectedValue)
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act 1
var addStatus = adapter.TryAdd(target, segment, resolver, value, out string errorMessage);
// Assert 1
Assert.True(addStatus);
Assert.Null(errorMessage);
Assert.Equal(value, target.NewProperty);
// Act 2
var removeStatus = adapter.TryRemove(target, segment, resolver, out string removeErrorMessage);
// Assert 2
Assert.True(removeStatus);
Assert.Null(removeErrorMessage);
Assert.Equal(expectedValue, target.NewProperty);
}
[Fact]
public void TryRemove_ThrowsPathNotFoundException_ForNonExistingProperty()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var removeStatus = adapter.TryRemove(target, segment, resolver, out string removeErrorMessage);
// Assert
Assert.False(removeStatus);
Assert.Equal($"The target location specified by path segment '{segment}' was not found.", removeErrorMessage);
}
[Fact]
public void TryTest_DoesNotThrowException_IfTestSuccessful()
{
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
var value = new List<object>()
{
"Joana",
2,
new Customer("Joana", 25)
};
target.NewProperty = value;
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
// Act
var testStatus = adapter.TryTest(target, segment, resolver, value, out string errorMessage);
// Assert
Assert.Equal(value, target.NewProperty);
Assert.True(testStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryTest_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
var adapter = new DynamicObjectAdapter();
dynamic target = new DynamicTestObject();
target.NewProperty = "Joana";
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
var expectedErrorMessage = $"The current value 'Joana' at path '{segment}' is not equal to the test value 'John'.";
// Act
var testStatus = adapter.TryTest(target, segment, resolver, "John", out string errorMessage);
// Assert
Assert.False(testStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
}
}

View File

@ -0,0 +1,497 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using Moq;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ListAdapterTest
{
[Fact]
public void Patch_OnArrayObject_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new[] { 20, 30 };
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, "0", resolver.Object, "40", out var message);
// Assert
Assert.False(addStatus);
Assert.Equal($"The type '{targetObject.GetType().FullName}' which is an array is not supported for json patch operations as it has a fixed size.", message);
}
[Fact]
public void Patch_OnNonGenericListObject_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new ArrayList();
targetObject.Add(20);
targetObject.Add(30);
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "40", out var message);
// Assert
Assert.False(addStatus);
Assert.Equal($"The type '{targetObject.GetType().FullName}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.", message);
}
[Fact]
public void Add_WithIndexSameAsNumberOfElements_Works()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<string>() { "James", "Mike" };
var listAdapter = new ListAdapter();
var position = targetObject.Count.ToString();
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "Rob", out var message);
// Assert
Assert.Null(message);
Assert.True(addStatus);
Assert.Equal(3, targetObject.Count);
Assert.Equal(new List<string>() { "James", "Mike", "Rob" }, targetObject);
}
[Theory]
[InlineData("-1")]
[InlineData("-2")]
[InlineData("3")]
public void Add_WithOutOfBoundsIndex_Fails(string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<string>() { "James", "Mike" };
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message);
// Assert
Assert.False(addStatus);
Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message);
}
[Theory]
[InlineData("_")]
[InlineData("blah")]
public void Patch_WithInvalidPositionFormat_Fails(string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<string>() { "James", "Mike" };
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message);
// Assert
Assert.False(addStatus);
Assert.Equal($"The path segment '{position}' is invalid for an array index.", message);
}
public static TheoryData<List<int>, List<int>> AppendAtEndOfListData
{
get
{
return new TheoryData<List<int>, List<int>>()
{
{
new List<int>() { },
new List<int>() { 20 }
},
{
new List<int>() { 5, 10 },
new List<int>() { 5, 10, 20 }
}
};
}
}
[Theory]
[MemberData(nameof(AppendAtEndOfListData))]
public void Add_Appends_AtTheEnd(List<int> targetObject, List<int> expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "20", out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
[Fact]
public void Add_NullObject_ToReferenceTypeListWorks()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
var targetObject = new List<string>() { "James", "Mike" };
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, value: null, errorMessage: out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(3, targetObject.Count);
Assert.Equal(new List<string>() { "James", "Mike", null }, targetObject);
}
[Fact]
public void Add_CompatibleTypeWorks()
{
// Arrange
var sDto = new SimpleObject();
var iDto = new InheritedObject();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<SimpleObject>() { sDto };
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, iDto, out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(2, targetObject.Count);
Assert.Equal(new List<SimpleObject>() { sDto, iDto }, targetObject);
}
[Fact]
public void Add_NonCompatibleType_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "James", out var message);
// Assert
Assert.False(addStatus);
Assert.Equal("The value 'James' is invalid for target location.", message);
}
public static TheoryData<IList, object, string, IList> AddingDifferentComplexTypeWorksData
{
get
{
return new TheoryData<IList, object, string, IList>()
{
{
new List<string>() { },
"a",
"-",
new List<string>() { "a" }
},
{
new List<string>() { "a", "b" },
"c",
"-",
new List<string>() { "a", "b", "c" }
},
{
new List<string>() { "a", "b" },
"c",
"0",
new List<string>() { "c", "a", "b" }
},
{
new List<string>() { "a", "b" },
"c",
"1",
new List<string>() { "a", "c", "b" }
}
};
}
}
[Theory]
[MemberData(nameof(AddingDifferentComplexTypeWorksData))]
public void Add_DifferentComplexTypeWorks(IList targetObject, object value, string position, IList expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
public static TheoryData<IList, object, string, IList> AddingKeepsObjectReferenceData
{
get
{
var sDto1 = new SimpleObject();
var sDto2 = new SimpleObject();
var sDto3 = new SimpleObject();
return new TheoryData<IList, object, string, IList>()
{
{
new List<SimpleObject>() { },
sDto1,
"-",
new List<SimpleObject>() { sDto1 }
},
{
new List<SimpleObject>() { sDto1, sDto2 },
sDto3,
"-",
new List<SimpleObject>() { sDto1, sDto2, sDto3 }
},
{
new List<SimpleObject>() { sDto1, sDto2 },
sDto3,
"0",
new List<SimpleObject>() { sDto3, sDto1, sDto2 }
},
{
new List<SimpleObject>() { sDto1, sDto2 },
sDto3,
"1",
new List<SimpleObject>() { sDto1, sDto3, sDto2 }
}
};
}
}
[Theory]
[MemberData(nameof(AddingKeepsObjectReferenceData))]
public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
[Theory]
[InlineData(new int[] { }, "0")]
[InlineData(new[] { 10, 20 }, "-1")]
[InlineData(new[] { 10, 20 }, "2")]
public void Get_IndexOutOfBounds(int[] input, string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>(input);
var listAdapter = new ListAdapter();
// Act
var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message);
// Assert
Assert.False(getStatus);
Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message);
}
[Theory]
[InlineData(new[] { 10, 20 }, "0", 10)]
[InlineData(new[] { 10, 20 }, "1", 20)]
[InlineData(new[] { 10 }, "0", 10)]
public void Get(int[] input, string position, object expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>(input);
var listAdapter = new ListAdapter();
// Act
var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message);
// Assert
Assert.True(getStatus);
Assert.Equal(expected, value);
Assert.Equal(new List<int>(input), targetObject);
}
[Theory]
[InlineData(new int[] { }, "0")]
[InlineData(new[] { 10, 20 }, "-1")]
[InlineData(new[] { 10, 20 }, "2")]
public void Remove_IndexOutOfBounds(int[] input, string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>(input);
var listAdapter = new ListAdapter();
// Act
var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message);
// Assert
Assert.False(removeStatus);
Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message);
}
[Theory]
[InlineData(new[] { 10, 20 }, "0", new[] { 20 })]
[InlineData(new[] { 10, 20 }, "1", new[] { 10 })]
[InlineData(new[] { 10 }, "0", new int[] { })]
public void Remove(int[] input, string position, int[] expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>(input);
var listAdapter = new ListAdapter();
// Act
var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message);
// Assert
Assert.True(removeStatus);
Assert.Equal(new List<int>(expected), targetObject);
}
[Fact]
public void Replace_NonCompatibleType_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "James", out var message);
// Assert
Assert.False(replaceStatus);
Assert.Equal("The value 'James' is invalid for target location.", message);
}
[Fact]
public void Replace_ReplacesValue_AtTheEnd()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "30", out var message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(new List<int>() { 10, 30 }, targetObject);
}
public static TheoryData<string, List<int>> ReplacesValuesAtPositionData
{
get
{
return new TheoryData<string, List<int>>()
{
{
"0",
new List<int>() { 30, 20 }
},
{
"1",
new List<int>() { 10, 30 }
}
};
}
}
[Theory]
[MemberData(nameof(ReplacesValuesAtPositionData))]
public void Replace_ReplacesValue_AtGivenPosition(string position, List<int> expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver.Object, "30", out var message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected, targetObject);
}
[Fact]
public void Test_DoesNotThrowException_IfTestIsSuccessful()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
// Act
var testStatus = listAdapter.TryTest(targetObject, "0", resolver.Object, "10", out var message);
//Assert
Assert.True(testStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
}
[Fact]
public void Test_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
var expectedErrorMessage = "The current value '20' at position '1' is not equal to the test value '10'.";
// Act
var testStatus = listAdapter.TryTest(targetObject, "1", resolver.Object, "10", out var errorMessage);
//Assert
Assert.False(testStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void Test_ThrowsJsonPatchException_IfListPositionOutOfBounds()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
var expectedErrorMessage = "The index value provided by path segment '2' is out of bounds of the array size.";
// Act
var testStatus = listAdapter.TryTest(targetObject, "2", resolver.Object, "10", out var errorMessage);
//Assert
Assert.False(testStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
}
}

View File

@ -0,0 +1,224 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitorTest
{
private class Class1
{
public string Name { get; set; }
public IList<string> States { get; set; } = new List<string>();
public IDictionary<string, string> Countries = new Dictionary<string, string>();
public dynamic Items { get; set; } = new ExpandoObject();
}
private class Class1Nested
{
public List<Class1> Customers { get; set; } = new List<Class1>();
}
public static IEnumerable<object[]> ReturnsListAdapterData
{
get
{
var model = new Class1();
yield return new object[] { model, "/States/-", model.States };
yield return new object[] { model.States, "/-", model.States };
var nestedModel = new Class1Nested();
nestedModel.Customers.Add(new Class1());
yield return new object[] { nestedModel, "/Customers/0/States/-", nestedModel.Customers[0].States };
yield return new object[] { nestedModel, "/Customers/0/States/0", nestedModel.Customers[0].States };
yield return new object[] { nestedModel.Customers, "/0/States/-", nestedModel.Customers[0].States };
yield return new object[] { nestedModel.Customers[0], "/States/-", nestedModel.Customers[0].States };
}
}
[Theory]
[MemberData(nameof(ReturnsListAdapterData))]
public void Visit_ValidPathToArray_ReturnsListAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver());
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
public static IEnumerable<object[]> ReturnsDictionaryAdapterData
{
get
{
var model = new Class1();
yield return new object[] { model, "/Countries/USA", model.Countries };
yield return new object[] { model.Countries, "/USA", model.Countries };
var nestedModel = new Class1Nested();
nestedModel.Customers.Add(new Class1());
yield return new object[] { nestedModel, "/Customers/0/Countries/USA", nestedModel.Customers[0].Countries };
yield return new object[] { nestedModel.Customers, "/0/Countries/USA", nestedModel.Customers[0].Countries };
yield return new object[] { nestedModel.Customers[0], "/Countries/USA", nestedModel.Customers[0].Countries };
}
}
[Theory]
[MemberData(nameof(ReturnsDictionaryAdapterData))]
public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver());
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.Equal(typeof(DictionaryAdapter<string, string>), adapter.GetType());
}
public static IEnumerable<object[]> ReturnsExpandoAdapterData
{
get
{
var nestedModel = new Class1Nested();
nestedModel.Customers.Add(new Class1());
yield return new object[] { nestedModel, "/Customers/0/Items/Name", nestedModel.Customers[0].Items };
yield return new object[] { nestedModel.Customers, "/0/Items/Name", nestedModel.Customers[0].Items };
yield return new object[] { nestedModel.Customers[0], "/Items/Name", nestedModel.Customers[0].Items };
}
}
[Theory]
[MemberData(nameof(ReturnsExpandoAdapterData))]
public void Visit_ValidPathToExpandoObject_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var contractResolver = new DefaultContractResolver();
var visitor = new ObjectVisitor(new ParsedPath(path), contractResolver);
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.Same(typeof(DictionaryAdapter<string, object>), adapter.GetType());
}
public static IEnumerable<object[]> ReturnsPocoAdapterData
{
get
{
var model = new Class1();
yield return new object[] { model, "/Name", model };
var nestedModel = new Class1Nested();
nestedModel.Customers.Add(new Class1());
yield return new object[] { nestedModel, "/Customers/0/Name", nestedModel.Customers[0] };
yield return new object[] { nestedModel.Customers, "/0/Name", nestedModel.Customers[0] };
yield return new object[] { nestedModel.Customers[0], "/Name", nestedModel.Customers[0] };
}
}
[Theory]
[MemberData(nameof(ReturnsPocoAdapterData))]
public void Visit_ValidPath_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver());
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<PocoAdapter>(adapter);
}
[Theory]
[InlineData("0")]
[InlineData("-1")]
public void Visit_InvalidIndexToArray_Fails(string position)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver());
var automobileDepartment = new Class1Nested();
object targetObject = automobileDepartment;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.False(visitStatus);
Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message);
}
[Theory]
[InlineData("-")]
[InlineData("foo")]
public void Visit_InvalidIndexFormatToArray_Fails(string position)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver());
var automobileDepartment = new Class1Nested();
object targetObject = automobileDepartment;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.False(visitStatus);
Assert.Equal($"The path segment '{position}' is invalid for an array index.", message);
}
[Fact]
public void Visit_DoesNotValidate_FinalPathSegment()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.IsType<PocoAdapter>(adapter);
}
[Fact]
public void Visit_NullTarget_ReturnsNullAdapter()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("test"), new DefaultContractResolver());
// Act
object target = null;
var visitStatus = visitor.TryVisit(ref target, out var adapter, out var message);
// Assert
Assert.False(visitStatus);
Assert.Null(adapter);
Assert.Null(message);
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ParsedPathTests
{
[Theory]
[InlineData("foo/bar~0baz", new string[] { "foo", "bar~baz" })]
[InlineData("foo/bar~00baz", new string[] { "foo", "bar~0baz" })]
[InlineData("foo/bar~01baz", new string[] { "foo", "bar~1baz" })]
[InlineData("foo/bar~10baz", new string[] { "foo", "bar/0baz" })]
[InlineData("foo/bar~1baz", new string[] { "foo", "bar/baz" })]
[InlineData("foo/bar~0/~0/~1~1/~0~0/baz", new string[] { "foo", "bar~", "~", "//", "~~", "baz" })]
[InlineData("~0~1foo", new string[] { "~/foo" })]
public void ParsingValidPathShouldSucceed(string path, string[] expected)
{
// Arrange & Act
var parsedPath = new ParsedPath(path);
// Assert
Assert.Equal(expected, parsedPath.Segments);
}
[Theory]
[InlineData("foo/bar~")]
[InlineData("~")]
[InlineData("~2")]
[InlineData("foo~3bar")]
public void PathWithInvalidEscapeSequenceShouldFail(string path)
{
// Arrange, Act & Assert
Assert.Throws<JsonPatchException>(() =>
{
var parsedPath = new ParsedPath(path);
});
}
}
}

View File

@ -0,0 +1,241 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class PocoAdapterTest
{
[Fact]
public void TryAdd_ReplacesExistingProperty()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
// Act
var addStatus = adapter.TryAdd(model, "Name", contractResolver, "John", out var errorMessage);
// Assert
Assert.Equal("John", model.Name);
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryAdd_ThrowsJsonPatchException_IfPropertyDoesNotExist()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found.";
// Act
var addStatus = adapter.TryAdd(model, "LastName", contractResolver, "Smith", out var errorMessage);
// Assert
Assert.False(addStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void TryGet_ExistingProperty()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
// Act
var getStatus = adapter.TryGet(model, "Name", contractResolver, out var value, out var errorMessage);
// Assert
Assert.Equal("Joana", value);
Assert.True(getStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryGet_ThrowsJsonPatchException_IfPropertyDoesNotExist()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found.";
// Act
var getStatus = adapter.TryGet(model, "LastName", contractResolver, out var value, out var errorMessage);
// Assert
Assert.Null(value);
Assert.False(getStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void TryRemove_SetsPropertyToNull()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
// Act
var removeStatus = adapter.TryRemove(model, "Name", contractResolver, out var errorMessage);
// Assert
Assert.Null(model.Name);
Assert.True(removeStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryRemove_ThrowsJsonPatchException_IfPropertyDoesNotExist()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found.";
// Act
var removeStatus = adapter.TryRemove(model, "LastName", contractResolver, out var errorMessage);
// Assert
Assert.False(removeStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void TryReplace_OverwritesExistingValue()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
// Act
var replaceStatus = adapter.TryReplace(model, "Name", contractResolver, "John", out var errorMessage);
// Assert
Assert.Equal("John", model.Name);
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryReplace_ThrowsJsonPatchException_IfNewValueIsInvalidType()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Age = 25
};
var expectedErrorMessage = "The value 'TwentySix' is invalid for target location.";
// Act
var replaceStatus = adapter.TryReplace(model, "Age", contractResolver, "TwentySix", out var errorMessage);
// Assert
Assert.Equal(25, model.Age);
Assert.False(replaceStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void TryReplace_ThrowsJsonPatchException_IfPropertyDoesNotExist()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found.";
// Act
var replaceStatus = adapter.TryReplace(model, "LastName", contractResolver, "Smith", out var errorMessage);
// Assert
Assert.Equal("Joana", model.Name);
Assert.False(replaceStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
[Fact]
public void TryTest_DoesNotThrowException_IfTestSuccessful()
{
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
// Act
var testStatus = adapter.TryTest(model, "Name", contractResolver, "Joana", out var errorMessage);
// Assert
Assert.Equal("Joana", model.Name);
Assert.True(testStatus);
Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message");
}
[Fact]
public void TryTest_ThrowsJsonPatchException_IfTestFails()
{
// Arrange
var adapter = new PocoAdapter();
var contractResolver = new DefaultContractResolver();
var model = new Customer
{
Name = "Joana"
};
var expectedErrorMessage = "The current value 'Joana' at path 'Name' is not equal to the test value 'John'.";
// Act
var testStatus = adapter.TryTest(model, "Name", contractResolver, "John", out var errorMessage);
// Assert
Assert.False(testStatus);
Assert.Equal(expectedErrorMessage, errorMessage);
}
private class Customer
{
public string Name { get; set; }
public int Age { get; set; }
}
}
}

View File

@ -0,0 +1,122 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch
{
public class JsonPatchDocumentGetPathTest
{
[Fact]
public void ExpressionType_MemberAccess()
{
// Arrange
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
// Act
var path = patchDocument.GetPath(p => p.SimpleObject.IntegerList, "-");
// Assert
Assert.Equal("/SimpleObject/IntegerList/-", path);
}
[Fact]
public void ExpressionType_ArrayIndex()
{
// Arrange
var patchDocument = new JsonPatchDocument<int[]>();
// Act
var path = patchDocument.GetPath(p => p[3], null);
// Assert
Assert.Equal("/3", path);
}
[Fact]
public void ExpressionType_Call()
{
// Arrange
var patchDocument = new JsonPatchDocument<Dictionary<string, int>>();
// Act
var path = patchDocument.GetPath(p => p["key"], "3");
// Assert
Assert.Equal("/key/3", path);
}
[Fact]
public void ExpressionType_Parameter_NullPosition()
{
// Arrange
var patchDocument = new JsonPatchDocument<SimpleObject>();
// Act
var path = patchDocument.GetPath(p => p, null);
// Assert
Assert.Equal("/", path);
}
[Fact]
public void ExpressionType_Parameter_WithPosition()
{
// Arrange
var patchDocument = new JsonPatchDocument<SimpleObject>();
// Act
var path = patchDocument.GetPath(p => p, "-");
// Assert
Assert.Equal("/-", path);
}
[Fact]
public void ExpressionType_Convert()
{
// Arrange
var patchDocument = new JsonPatchDocument<NestedObjectWithDerivedClass>();
// Act
var path = patchDocument.GetPath(p => (BaseClass)p.DerivedObject, null);
// Assert
Assert.Equal("/DerivedObject", path);
}
[Fact]
public void ExpressionType_NotSupported()
{
// Arrange
var patchDocument = new JsonPatchDocument<SimpleObject>();
// Act
var exception = Assert.Throws<InvalidOperationException>(() =>
{
patchDocument.GetPath(p => p.IntegerValue >= 4, null);
});
// Assert
Assert.Equal("The expression '(p.IntegerValue >= 4)' is not supported. Supported expressions include member access and indexer expressions.", exception.Message);
}
}
internal class DerivedClass : BaseClass
{
public DerivedClass()
{
}
}
internal class NestedObjectWithDerivedClass
{
public DerivedClass DerivedObject { get; set; }
}
internal class BaseClass
{
}
}

View File

@ -0,0 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch
{
public class JsonPatchDocumentJsonPropertyAttributeTest
{
[Fact]
public void Add_RespectsJsonPropertyAttribute()
{
// Arrange
var patchDocument = new JsonPatchDocument<JsonPropertyObject>();
// Act
patchDocument.Add(p => p.Name, "John");
// Assert
var pathToCheck = patchDocument.Operations.First().path;
Assert.Equal("/AnotherName", pathToCheck);
}
[Fact]
public void Add_RespectsJsonPropertyAttribute_WithDotWhitespaceAndBackslashInName()
{
// Arrange
var obj = new JsonPropertyObjectWithStrangeNames();
var patchDocument = new JsonPatchDocument();
// Act
patchDocument.Add("/First Name.", "John");
patchDocument.Add("Last\\Name", "Doe");
patchDocument.ApplyTo(obj);
// Assert
Assert.Equal("John", obj.FirstName);
Assert.Equal("Doe", obj.LastName);
}
[Fact]
public void Move_FallsbackToPropertyName_WhenJsonPropertyAttributeName_IsEmpty()
{
// Arrange
var patchDocument = new JsonPatchDocument<JsonPropertyWithNoPropertyName>();
// Act
patchDocument.Move(m => m.StringProperty, m => m.StringProperty2);
// Assert
var fromPath = patchDocument.Operations.First().from;
Assert.Equal("/StringProperty", fromPath);
var toPath = patchDocument.Operations.First().path;
Assert.Equal("/StringProperty2", toPath);
}
private class JsonPropertyObject
{
[JsonProperty("AnotherName")]
public string Name { get; set; }
}
private class JsonPropertyObjectWithStrangeNames
{
[JsonProperty("First Name.")]
public string FirstName { get; set; }
[JsonProperty("Last\\Name")]
public string LastName { get; set; }
}
private class JsonPropertyWithNoPropertyName
{
[JsonProperty]
public string StringProperty { get; set; }
[JsonProperty]
public string[] ArrayProperty { get; set; }
[JsonProperty]
public string StringProperty2 { get; set; }
[JsonProperty]
public string SSN { get; set; }
}
}
}

View File

@ -0,0 +1,162 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch
{
public class JsonPatchDocumentTest
{
[Fact]
public void InvalidPathAtBeginningShouldThrowException()
{
// Arrange
var patchDocument = new JsonPatchDocument();
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.Add("//NewInt", 1);
});
// Assert
Assert.Equal(
"The provided string '//NewInt' is an invalid path.",
exception.Message);
}
[Fact]
public void InvalidPathAtEndShouldThrowException()
{
// Arrange
var patchDocument = new JsonPatchDocument();
// Act
var exception = Assert.Throws<JsonPatchException>(() =>
{
patchDocument.Add("NewInt//", 1);
});
// Assert
Assert.Equal(
"The provided string 'NewInt//' is an invalid path.",
exception.Message);
}
[Fact]
public void NonGenericPatchDocToGenericMustSerialize()
{
// Arrange
var targetObject = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocument = new JsonPatchDocument();
patchDocument.Copy("StringProperty", "AnotherStringProperty");
var serialized = JsonConvert.SerializeObject(patchDocument);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObject>>(serialized);
// Act
deserialized.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.AnotherStringProperty);
}
[Fact]
public void GenericPatchDocToNonGenericMustSerialize()
{
// Arrange
var targetObject = new SimpleObject()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var patchDocTyped = new JsonPatchDocument<SimpleObject>();
patchDocTyped.Copy(o => o.StringProperty, o => o.AnotherStringProperty);
var patchDocUntyped = new JsonPatchDocument();
patchDocUntyped.Copy("StringProperty", "AnotherStringProperty");
var serializedTyped = JsonConvert.SerializeObject(patchDocTyped);
var serializedUntyped = JsonConvert.SerializeObject(patchDocUntyped);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument>(serializedTyped);
// Act
deserialized.ApplyTo(targetObject);
// Assert
Assert.Equal("A", targetObject.AnotherStringProperty);
}
[Fact]
public void Deserialization_Successful_ForValidJsonPatchDocument()
{
// Arrange
var doc = new SimpleObject()
{
StringProperty = "A",
DecimalValue = 10,
DoubleValue = 10,
FloatValue = 10,
IntegerValue = 10
};
var patchDocument = new JsonPatchDocument<SimpleObject>();
patchDocument.Replace(o => o.StringProperty, "B");
patchDocument.Replace(o => o.DecimalValue, 12);
patchDocument.Replace(o => o.DoubleValue, 12);
patchDocument.Replace(o => o.FloatValue, 12);
patchDocument.Replace(o => o.IntegerValue, 12);
// default: no envelope
var serialized = JsonConvert.SerializeObject(patchDocument);
// Act
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObject>>(serialized);
// Assert
Assert.IsType<JsonPatchDocument<SimpleObject>>(deserialized);
}
[Fact]
public void Deserialization_Fails_ForInvalidJsonPatchDocument()
{
// Arrange
var serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}";
// Act
var exception = Assert.Throws<JsonSerializationException>(() =>
{
var deserialized
= JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
});
// Assert
Assert.Equal("The JSON patch document was malformed and could not be parsed.", exception.Message);
}
[Fact]
public void Deserialization_Fails_ForInvalidTypedJsonPatchDocument()
{
// Arrange
var serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}";
// Act
var exception = Assert.Throws<JsonSerializationException>(() =>
{
var deserialized
= JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObject>>(serialized);
});
// Assert
Assert.Equal("The JSON patch document was malformed and could not be parsed.", exception.Message);
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.JsonPatch" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Operations
{
public class OperationBaseTests
{
[Theory]
[InlineData("ADd", OperationType.Add)]
[InlineData("Copy", OperationType.Copy)]
[InlineData("mOVE", OperationType.Move)]
[InlineData("REMOVE", OperationType.Remove)]
[InlineData("replace", OperationType.Replace)]
[InlineData("TeSt", OperationType.Test)]
public void SetValidOperationType(string op, OperationType operationType)
{
// Arrange
var operationBase = new OperationBase();
operationBase.op = op;
// Act & Assert
Assert.Equal(operationType, operationBase.OperationType);
}
[Theory]
[InlineData("invalid", OperationType.Invalid)]
[InlineData("coppy", OperationType.Invalid)]
[InlineData("notvalid", OperationType.Invalid)]
public void InvalidOperationType_SetsOperationTypeInvalid(string op, OperationType operationType)
{
// Arrange
var operationBase = new OperationBase();
operationBase.op = op;
// Act & Assert
Assert.Equal(operationType, operationBase.OperationType);
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch
{
public class TestErrorLogger<T> where T : class
{
public string ErrorMessage { get; set; }
public void LogErrorMessage(JsonPatchError patchError)
{
ErrorMessage = patchError.ErrorMessage;
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal class Customer
{
private string _name;
private int _age;
public Customer(string name, int age)
{
_name = name;
_age = age;
}
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Dynamic;
namespace Microsoft.AspNetCore.JsonPatch
{
public class DynamicTestObject : DynamicObject
{
private Dictionary<string, object> _dictionary = new Dictionary<string, object>();
public object this[string key] { get => ((IDictionary<string, object>)_dictionary)[key]; set => ((IDictionary<string, object>)_dictionary)[key] = value; }
public ICollection<string> Keys => ((IDictionary<string, object>)_dictionary).Keys;
public ICollection<object> Values => ((IDictionary<string, object>)_dictionary).Values;
public int Count => ((IDictionary<string, object>)_dictionary).Count;
public bool IsReadOnly => ((IDictionary<string, object>)_dictionary).IsReadOnly;
public void Add(string key, object value)
{
((IDictionary<string, object>)_dictionary).Add(key, value);
}
public void Add(KeyValuePair<string, object> item)
{
((IDictionary<string, object>)_dictionary).Add(item);
}
public void Clear()
{
((IDictionary<string, object>)_dictionary).Clear();
}
public bool Contains(KeyValuePair<string, object> item)
{
return ((IDictionary<string, object>)_dictionary).Contains(item);
}
public bool ContainsKey(string key)
{
return ((IDictionary<string, object>)_dictionary).ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
((IDictionary<string, object>)_dictionary).CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return ((IDictionary<string, object>)_dictionary).GetEnumerator();
}
public bool Remove(string key)
{
return ((IDictionary<string, object>)_dictionary).Remove(key);
}
public bool Remove(KeyValuePair<string, object> item)
{
return ((IDictionary<string, object>)_dictionary).Remove(item);
}
public bool TryGetValue(string key, out object value)
{
return ((IDictionary<string, object>)_dictionary).TryGetValue(key, out value);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var name = binder.Name;
return TryGetValue(name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
_dictionary[binder.Name] = value;
return true;
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch
{
public class InheritedObject : SimpleObject
{
public string AdditionalStringProperty { get; set; }
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch
{
public class NestedObject
{
public string StringProperty { get; set; }
public dynamic DynamicProperty { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch
{
public class SimpleObject
{
public List<SimpleObject> SimpleObjectList { get; set; }
public List<int> IntegerList { get; set; }
public IList<int> IntegerIList { get; set; }
public int IntegerValue { get; set; }
public int AnotherIntegerValue { get; set; }
public string StringProperty { get; set; }
public string AnotherStringProperty { get; set; }
public decimal DecimalValue { get; set; }
public double DoubleValue { get; set; }
public float FloatValue { get; set; }
public Guid GuidValue { get; set; }
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch
{
public class SimpleObjectWithNestedObject
{
public int IntegerValue { get; set; }
public NestedObject NestedObject { get; set; }
public SimpleObject SimpleObject { get; set; }
public InheritedObject InheritedObject { get; set; }
public List<SimpleObject> SimpleObjectList { get; set; }
public IList<SimpleObject> SimpleObjectIList { get; set; }
public SimpleObjectWithNestedObject()
{
NestedObject = new NestedObject();
SimpleObject = new SimpleObject();
InheritedObject = new InheritedObject();
SimpleObjectList = new List<SimpleObject>();
}
}
}

View File

@ -0,0 +1,120 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSockets", "WebSockets", "{E0D9867D-C23D-43EB-8D9C-DE0398A25432}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{A86EE055-ACD3-4BAC-A51D-1B3C71067AE0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoApp", "WebSockets\samples\EchoApp\EchoApp.csproj", "{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServer", "WebSockets\samples\TestServer\TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets", "WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj", "{BECAA6A1-1AA4-415E-ADF3-07C103333826}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutobahnTestApp", "WebSockets\test\AutobahnTestApp\AutobahnTestApp.csproj", "{76B25812-AAFB-45BA-A71A-24F0C654ADFB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.ConformanceTests", "WebSockets\test\ConformanceTests\Microsoft.AspNetCore.WebSockets.ConformanceTests.csproj", "{88BDEE69-4DE3-40B5-A478-677EA355FB52}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Tests", "WebSockets\test\UnitTests\Microsoft.AspNetCore.WebSockets.Tests.csproj", "{93970702-1BDB-4A8C-B7F6-020294464BB6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|x64.ActiveCfg = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|x64.Build.0 = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|x86.ActiveCfg = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Debug|x86.Build.0 = Debug|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|Any CPU.Build.0 = Release|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|x64.ActiveCfg = Release|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|x64.Build.0 = Release|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|x86.ActiveCfg = Release|Any CPU
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E}.Release|x86.Build.0 = Release|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.Build.0 = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.Build.0 = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.Build.0 = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.Build.0 = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.ActiveCfg = Debug|Any CPU
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.Build.0 = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|x64.ActiveCfg = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|x64.Build.0 = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|x86.ActiveCfg = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Debug|x86.Build.0 = Debug|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|Any CPU.Build.0 = Release|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|x64.ActiveCfg = Release|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|x64.Build.0 = Release|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|x86.ActiveCfg = Release|Any CPU
{BECAA6A1-1AA4-415E-ADF3-07C103333826}.Release|x86.Build.0 = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|x64.ActiveCfg = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|x64.Build.0 = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|x86.ActiveCfg = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Debug|x86.Build.0 = Debug|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|Any CPU.Build.0 = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|x64.ActiveCfg = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|x64.Build.0 = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|x86.ActiveCfg = Release|Any CPU
{76B25812-AAFB-45BA-A71A-24F0C654ADFB}.Release|x86.Build.0 = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|x64.ActiveCfg = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|x64.Build.0 = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|x86.ActiveCfg = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Debug|x86.Build.0 = Debug|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|Any CPU.Build.0 = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|x64.ActiveCfg = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|x64.Build.0 = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|x86.ActiveCfg = Release|Any CPU
{88BDEE69-4DE3-40B5-A478-677EA355FB52}.Release|x86.Build.0 = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|x64.ActiveCfg = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|x64.Build.0 = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|x86.ActiveCfg = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Debug|x86.Build.0 = Debug|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|Any CPU.Build.0 = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|x64.ActiveCfg = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|x64.Build.0 = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|x86.ActiveCfg = Release|Any CPU
{93970702-1BDB-4A8C-B7F6-020294464BB6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A86EE055-ACD3-4BAC-A51D-1B3C71067AE0} = {E0D9867D-C23D-43EB-8D9C-DE0398A25432}
{0792C20B-1D18-4D7C-9C0F-A6F45A0F378E} = {A86EE055-ACD3-4BAC-A51D-1B3C71067AE0}
{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} = {A86EE055-ACD3-4BAC-A51D-1B3C71067AE0}
{BECAA6A1-1AA4-415E-ADF3-07C103333826} = {E0D9867D-C23D-43EB-8D9C-DE0398A25432}
{76B25812-AAFB-45BA-A71A-24F0C654ADFB} = {E0D9867D-C23D-43EB-8D9C-DE0398A25432}
{88BDEE69-4DE3-40B5-A478-677EA355FB52} = {E0D9867D-C23D-43EB-8D9C-DE0398A25432}
{93970702-1BDB-4A8C-B7F6-020294464BB6} = {E0D9867D-C23D-43EB-8D9C-DE0398A25432}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,12 @@
WebSockets
==========
Contains a managed implementation of the WebSocket protocol, along with server integration components.
## System Requirements
This repo has a few special system requirements/prerequisites.
1. Windows IIS Express tests require IIS Express 10 and Windows 8 for WebSockets support
2. HttpListener/ASP.NET 4.6 samples require at least Windows 8
3. Autobahn Test Suite requires special installation see the README.md in [test/AutobahnTestApp](./test/AutobahnTestApp/README.md)

Some files were not shown because too many files have changed in this diff Show More