├── .editorconfig
├── .gitignore
├── CODEOFCONDUCT.md
├── CONTRIBUTING.md
├── HttpPatch.png
├── LICENSE
├── README.md
├── Simple.HttpPatch.sln
├── build.cake
├── build.ps1
├── build.sh
├── samples
└── Simple.HttpPatch.Samples
│ ├── Controllers
│ └── ValuesController.cs
│ ├── Model
│ └── Person.cs
│ ├── Program.cs
│ └── Simple.HttpPatch.Samples.csproj
├── src
└── Simple.HttpPatch
│ ├── DynamicMemberBinder.cs
│ ├── Patch.cs
│ ├── PatchCollection.cs
│ ├── PatchIgnoreAttribute.cs
│ ├── PatchIgnoreNullAttribute.cs
│ ├── PatchModelBinderProvider.cs
│ ├── PatchObjectModelBinder.cs
│ └── Simple.HttpPatch.csproj
└── tests
└── Simple.HttpPatch.Tests
└── Simple.HttpPatch.Tests.csproj
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = crlf
6 | indent_style = space
7 | indent_size = 2
8 |
9 | [*.sql]
10 | indent_size = 4
11 |
12 | [*.cs]
13 | indent_size = 4
14 |
15 | # Prefer "var"
16 | csharp_style_var_when_type_is_apparent = true : suggestion
17 |
18 | # Prefer method-like constructs to have a block body
19 | csharp_style_expression_bodied_methods = false : none
20 |
21 | # Prefer property-like constructs to have an expression-body
22 | csharp_style_expression_bodied_properties = true : none
23 | csharp_style_expression_bodied_accessors = true : none
24 |
25 | # Suggest more modern language features when available
26 | csharp_style_inlined_variable_declaration = true : suggestion
27 | csharp_style_throw_expression = true : suggestion
28 |
29 | # Newline settings
30 | csharp_new_line_before_open_brace = all
31 | csharp_new_line_before_else = true
32 | csharp_new_line_before_catch = true
33 | csharp_new_line_before_finally = true
34 |
35 | # Use language keywords instead of framework type names for type references
36 | dotnet_style_predefined_type_for_locals_parameters_members = true : suggestion
37 |
38 | # Suggest more modern language features when available
39 | dotnet_style_object_initializer = true : suggestion
40 | dotnet_style_collection_initializer = true : suggestion
41 | dotnet_style_coalesce_expression = true : suggestion
42 | dotnet_style_null_propagation = true : suggestion
43 |
44 | # Sort using and Import directives with System.* appearing first
45 | dotnet_sort_system_directives_first = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # use glob syntax
2 | syntax: glob
3 |
4 | *.obj
5 | *.pdb
6 | *.user
7 | *.aps
8 | *.pch
9 | *.vspscc
10 | *.vssscc
11 | *_i.c
12 | *_p.c
13 | *.ncb
14 | *.suo
15 | *.tlb
16 | *.tlh
17 | *.bak
18 | *.cache
19 | *.ilk
20 | *.log
21 | *.lib
22 | *.sbr
23 | *.scc
24 | [Bb]in
25 | [Dd]ebug*/
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]humbs.db
30 | [Tt]est[Rr]esult*
31 | [Bb]uild[Ll]og.*
32 | *.[Pp]ublish.xml
33 | *.resharper
34 | *.received.txt
35 | *.orig
36 | packages/
37 | nuget.exe
38 | docs/guidehtml
39 | docs/apihtml
40 |
41 | .idea/
42 | *.iml
43 |
44 | **/BenchmarkDotNet.Artifacts/*
45 | **/project.lock.json
46 | tests/output/*
47 | .vs/restore.dg
48 | artifacts/*
49 | **/Properties/launchSettings.json
50 | BDN.Generated
51 | BenchmarkDotNet.Samples/Properties/launchSettings.json
52 | src/BenchmarkDotNet.Core/Disassemblers/*
53 |
54 | # Visual Studio 2015 cache/options directory
55 | .vs/
56 |
57 | # Cake
58 | tools/**
59 | .dotnet
--------------------------------------------------------------------------------
/CODEOFCONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at romamarusyk@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a [code of conduct](https://github.com/Marusyk/Simple.HttpPatch/blob/master/CODEOFCONDUCT.md), please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes new environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent.
16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
17 | do not have permission to do that, you may request the second reviewer to merge it for you.
18 |
--------------------------------------------------------------------------------
/HttpPatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marusyk/Simple.HttpPatch/780d505338dafc714d90f7a09d28ab3d39d12ff7/HttpPatch.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Roma Marusyk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://ci.appveyor.com/project/Marusyk/simple-httppatch) [](https://github.com/Marusyk/Simple.HttpPatch/releases/tag/v1.0.0-beta) [](https://www.nuget.org/packages/Simple.HttpPatch)
6 | []()
7 | [](LICENSE.md) 
8 |
9 | # Simple.HttpPatch [](https://stand-with-ukraine.pp.ua)
10 |
11 | Simple.HttpPatch is implementation for .NET (Full framework and Core) to easily allow & apply partial RESTful service (through Web API) using [HTTP PATCH](https://tools.ietf.org/html/rfc5789) method.
12 |
13 | ## Installation
14 |
15 | You can install the latest version via [NuGet](https://www.nuget.org/packages/Simple.HttpPatch/).
16 |
17 | `PM> Install-Package Simple.HttpPatch`
18 |
19 | ## How to use
20 |
21 | See [samples](https://github.com/Marusyk/Simple.HttpPatch/tree/master/samples/Simple.HttpPatch.Samples) folder to learn of to use this library with ASP.NET Core.
22 |
23 | Patch a single entity
24 |
25 | ```C#
26 | [HttpPatch]
27 | public Person Patch([FromBody] Patch personPatch)
28 | {
29 | var person = _repo.GetPersonById(1);
30 | personPatch.Apply(person);
31 | return person;
32 | }
33 | ```
34 |
35 | To exclude properties of an entity while applying the changes to the original entity use `PatchIgnoreAttribute`.
36 | When your property is a reference type (which allows null) but you don't want that null overwrites your previous stored data then use `PatchIgnoreNullAttribute`
37 |
38 | ```C#
39 | public class Person
40 | {
41 | public int Id { get; set; }
42 | [PatchIgnore]
43 | public string Name { get; set; }
44 | public int? Age { get; set; }
45 | [PatchIgnoreNull]
46 | public DateTime BirthDate { get; set; }
47 | }
48 | ```
49 |
50 | *Note: The property with name `Id` is excluded by default*
51 |
52 | For firewalls that don't support `PATCH` see [this issue](https://github.com/Marusyk/Simple.HttpPatch/issues/5)
53 |
54 | ## Contributing
55 |
56 | Please read [CONTRIBUTING.md](https://github.com/Marusyk/Simple.HttpPatch/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
57 |
58 | ## License
59 |
60 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/Marusyk/Simple.HttpPatch/blob/master/LICENSE) file for details
61 |
--------------------------------------------------------------------------------
/Simple.HttpPatch.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio 15
3 | VisualStudioVersion = 15.0.26730.16
4 | MinimumVisualStudioVersion = 15.0.26124.0
5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5AD0420-D2B8-4412-98FE-1B994A7334C3}"
6 | EndProject
7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Simple.HttpPatch", "src\Simple.HttpPatch\Simple.HttpPatch.csproj", "{50B9292F-6E6A-4879-AC64-66EE572CA57D}"
8 | EndProject
9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1E00F94D-2022-4C1F-A9B6-811F61A53F28}"
10 | EndProject
11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Simple.HttpPatch.Samples", "samples\Simple.HttpPatch.Samples\Simple.HttpPatch.Samples.csproj", "{34F9CE42-2F86-449C-99A0-8B5BD5D5B331}"
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5FC208A4-EF9F-4333-8A98-F6BF340BC1C0}"
14 | EndProject
15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Simple.HttpPatch.Tests", "tests\Simple.HttpPatch.Tests\Simple.HttpPatch.Tests.csproj", "{1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA}"
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {50B9292F-6E6A-4879-AC64-66EE572CA57D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {50B9292F-6E6A-4879-AC64-66EE572CA57D}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {50B9292F-6E6A-4879-AC64-66EE572CA57D}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {50B9292F-6E6A-4879-AC64-66EE572CA57D}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {34F9CE42-2F86-449C-99A0-8B5BD5D5B331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {34F9CE42-2F86-449C-99A0-8B5BD5D5B331}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {34F9CE42-2F86-449C-99A0-8B5BD5D5B331}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {34F9CE42-2F86-449C-99A0-8B5BD5D5B331}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | GlobalSection(NestedProjects) = preSolution
40 | {50B9292F-6E6A-4879-AC64-66EE572CA57D} = {A5AD0420-D2B8-4412-98FE-1B994A7334C3}
41 | {34F9CE42-2F86-449C-99A0-8B5BD5D5B331} = {1E00F94D-2022-4C1F-A9B6-811F61A53F28}
42 | {1FEBC9EB-A1EE-43BA-AF8E-930A8F11BECA} = {5FC208A4-EF9F-4333-8A98-F6BF340BC1C0}
43 | EndGlobalSection
44 | GlobalSection(ExtensibilityGlobals) = postSolution
45 | SolutionGuid = {FA4BB587-8213-4F01-B3B1-06FB4296E038}
46 | EndGlobalSection
47 | EndGlobal
48 |
--------------------------------------------------------------------------------
/build.cake:
--------------------------------------------------------------------------------
1 | // ARGUMENTS
2 | var target = Argument("Target", "Default");
3 | var configuration = Argument("Configuration", "Release");
4 | var nugetApiKey = Argument("NugetApiKey", "");
5 |
6 | // GLOBAL VARIABLES
7 | var artifactsDirectory = Directory("./artifacts");
8 | var solutionFile = "./Simple.HttpPatch.sln";
9 | var solutionFileBackup = solutionFile + ".build.backup";
10 | var isRunningOnWindows = IsRunningOnWindows();
11 | var IsOnAppVeyorAndNotPR = AppVeyor.IsRunningOnAppVeyor && !AppVeyor.Environment.PullRequest.IsPullRequest;
12 |
13 | Setup(_ =>
14 | {
15 | StartProcess("dotnet", new ProcessSettings { Arguments = "--info" });
16 | if(!isRunningOnWindows)
17 | {
18 | StartProcess("mono", new ProcessSettings { Arguments = "--version" });
19 |
20 | // create solution backup
21 | CopyFile(solutionFile, solutionFileBackup);
22 | }
23 | });
24 |
25 | Teardown(_ =>
26 | {
27 | if(!isRunningOnWindows && BuildSystem.IsLocalBuild)
28 | {
29 | if(FileExists(solutionFileBackup)) // Revert back solution file
30 | {
31 | DeleteFile(solutionFile);
32 | MoveFile(solutionFileBackup, solutionFile);
33 | }
34 | }
35 | });
36 |
37 | Task("Clean")
38 | .Does(() =>
39 | {
40 | CleanDirectory(artifactsDirectory);
41 |
42 | if(BuildSystem.IsLocalBuild)
43 | {
44 | CleanDirectories(GetDirectories("./**/obj") + GetDirectories("./**/bin"));
45 | }
46 | });
47 |
48 | Task("Restore")
49 | .IsDependentOn("Clean")
50 | .Does(() =>
51 | {
52 | DotNetCoreRestore(solutionFile);
53 | });
54 |
55 | Task("Build")
56 | .IsDependentOn("Restore")
57 | .Does(() =>
58 | {
59 | var buildSettings = new MSBuildSettings()
60 | .SetConfiguration(configuration)
61 | .WithTarget("Rebuild")
62 | .SetVerbosity(Verbosity.Minimal)
63 | .UseToolVersion(MSBuildToolVersion.Default)
64 | .SetMSBuildPlatform(MSBuildPlatform.Automatic)
65 | .SetPlatformTarget(PlatformTarget.MSIL) // Any CPU
66 | .SetNodeReuse(true);
67 |
68 | if(!isRunningOnWindows)
69 | {
70 | // hack for Linux bug - missing MSBuild path
71 | if(new CakePlatform().Family == PlatformFamily.Linux)
72 | {
73 | var monoVersion = GetMonoVersionMoniker();
74 | var isMSBuildSupported = monoVersion != null && System.Text.RegularExpressions.Regex.IsMatch(monoVersion,@"([5-9]|\d{2,})\.\d+\.\d+(\.\d+)?");
75 | if(isMSBuildSupported)
76 | {
77 | buildSettings.ToolPath = new FilePath(@"/usr/lib/mono/msbuild/15.0/bin/MSBuild.dll");
78 | Information(string.Format("Mono supports MSBuild. Override ToolPath value with `{0}`", buildSettings.ToolPath));
79 | }
80 | }
81 | }
82 |
83 | var path = MakeAbsolute(new DirectoryPath(solutionFile));
84 | MSBuild(path.FullPath, buildSettings);
85 | });
86 |
87 | Task("Pack")
88 | .IsDependentOn("Restore")
89 | .WithCriteria(IsOnAppVeyorAndNotPR || !string.IsNullOrEmpty(nugetApiKey))
90 | .Does(() =>
91 | {
92 | var settings = new DotNetCorePackSettings
93 | {
94 | Configuration = configuration,
95 | OutputDirectory = artifactsDirectory
96 | };
97 |
98 | var projects = GetFiles("./src/**/*.csproj");
99 | foreach(var project in projects)
100 | {
101 | DotNetCorePack(project.FullPath, settings);
102 | }
103 | });
104 |
105 | Task("Publish")
106 | .IsDependentOn("Pack")
107 | .WithCriteria(IsOnAppVeyorAndNotPR || !string.IsNullOrEmpty(nugetApiKey))
108 | .Does(() =>
109 | {
110 | var dir = string.Concat(artifactsDirectory, @"\*.nupkg");
111 | NuGetPush(GetFiles(dir).First(), new NuGetPushSettings {
112 | Source = "https://www.nuget.org/api/v2/package",
113 | ApiKey = nugetApiKey
114 | });
115 | });
116 |
117 | Task("Default")
118 | .IsDependentOn("Clean")
119 | .IsDependentOn("Restore")
120 | .IsDependentOn("Build")
121 | .IsDependentOn("Pack")
122 | .IsDependentOn("Publish");
123 |
124 | RunTarget(target);
125 |
126 | // HELPERS
127 |
128 | private string GetMonoVersionMoniker()
129 | {
130 | var type = Type.GetType("Mono.Runtime");
131 | if (type != null)
132 | {
133 | var displayName = type.GetMethod("GetDisplayName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
134 | if (displayName != null)
135 | {
136 | return displayName.Invoke(null, null).ToString();
137 | }
138 | }
139 | return null;
140 | }
141 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | This is a Powershell script to bootstrap a Cake build.
4 | .DESCRIPTION
5 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
6 | and execute your Cake build script with the parameters you provide.
7 | .PARAMETER Target
8 | The build script target to run.
9 | .PARAMETER Configuration
10 | The build configuration to use.
11 | .PARAMETER Verbosity
12 | Specifies the amount of information to be displayed.
13 | .PARAMETER WhatIf
14 | Performs a dry run of the build script.
15 | No tasks will be executed.
16 | .PARAMETER ScriptArgs
17 | Remaining arguments are added here.
18 | .LINK
19 | http://cakebuild.net
20 | #>
21 |
22 | [CmdletBinding()]
23 | Param(
24 | [string]$Target = "Default",
25 | [ValidateSet("Release", "Debug")]
26 | [string]$Configuration = "Release",
27 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
28 | [string]$Verbosity = "Verbose",
29 | [switch]$WhatIf,
30 | [string]$NugetApiKey,
31 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
32 | [string[]]$ScriptArgs
33 | )
34 |
35 | $CakeVersion = "0.21.1"
36 | $DotNetChannel = "preview";
37 | $DotNetVersion = "2.0.0";
38 | $DotNetInstallerUri = "https://dot.net/v1/dotnet-install.ps1";
39 | $NugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
40 |
41 | # Make sure tools folder exists
42 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
43 | $ToolPath = Join-Path $PSScriptRoot "tools"
44 | if (!(Test-Path $ToolPath)) {
45 | Write-Verbose "Creating tools directory..."
46 | New-Item -Path $ToolPath -Type directory | out-null
47 | }
48 |
49 | ###########################################################################
50 | # INSTALL .NET CORE CLI
51 | ###########################################################################
52 |
53 | Function Remove-PathVariable([string]$VariableToRemove)
54 | {
55 | $path = [Environment]::GetEnvironmentVariable("PATH", "User")
56 | if ($path -ne $null)
57 | {
58 | $newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove }
59 | [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "User")
60 | }
61 |
62 | $path = [Environment]::GetEnvironmentVariable("PATH", "Process")
63 | if ($path -ne $null)
64 | {
65 | $newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove }
66 | [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "Process")
67 | }
68 | }
69 |
70 | # Get .NET Core CLI path if installed.
71 | $FoundDotNetCliVersion = $null;
72 | if (Get-Command dotnet -ErrorAction SilentlyContinue) {
73 | $FoundDotNetCliVersion = dotnet --version;
74 | }
75 |
76 | if($FoundDotNetCliVersion -ne $DotNetVersion) {
77 | $InstallPath = Join-Path $PSScriptRoot ".dotnet"
78 | if (!(Test-Path $InstallPath)) {
79 | mkdir -Force $InstallPath | Out-Null;
80 | }
81 | (New-Object System.Net.WebClient).DownloadFile($DotNetInstallerUri, "$InstallPath\dotnet-install.ps1");
82 | & $InstallPath\dotnet-install.ps1 -Channel $DotNetChannel -Version $DotNetVersion -InstallDir $InstallPath;
83 |
84 | Remove-PathVariable "$InstallPath"
85 | $env:PATH = "$InstallPath;$env:PATH"
86 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
87 | $env:DOTNET_CLI_TELEMETRY_OPTOUT=1
88 | }
89 |
90 | ###########################################################################
91 | # INSTALL NUGET
92 | ###########################################################################
93 |
94 | # Make sure nuget.exe exists.
95 | $NugetPath = Join-Path $ToolPath "nuget.exe"
96 | if (!(Test-Path $NugetPath)) {
97 | Write-Host "Downloading NuGet.exe..."
98 | (New-Object System.Net.WebClient).DownloadFile($NugetUrl, $NugetPath);
99 | }
100 |
101 | ###########################################################################
102 | # INSTALL CAKE
103 | ###########################################################################
104 |
105 | # Make sure Cake has been installed.
106 | $CakePath = Join-Path $ToolPath "Cake.$CakeVersion/Cake.exe"
107 | if (!(Test-Path $CakePath)) {
108 | Write-Host "Installing Cake..."
109 | Invoke-Expression "&`"$NugetPath`" install Cake -Version $CakeVersion -OutputDirectory `"$ToolPath`"" | Out-Null;
110 | if ($LASTEXITCODE -ne 0) {
111 | Throw "An error occured while restoring Cake from NuGet."
112 | }
113 | }
114 |
115 | ###########################################################################
116 | # RUN BUILD SCRIPT
117 | ###########################################################################
118 |
119 | # Build the argument list.
120 | $Arguments = @{
121 | target=$Target;
122 | configuration=$Configuration;
123 | verbosity=$Verbosity;
124 | dryrun=$WhatIf;
125 | nugetapikey=$NugetApiKey;
126 | }.GetEnumerator() | %{"--{0}=`"{1}`"" -f $_.key, $_.value };
127 |
128 | # Start Cake
129 | Write-Host "Running build script..."
130 | Invoke-Expression "& `"$CakePath`" `"build.cake`" $Arguments $ScriptArgs"
131 | exit $LASTEXITCODE
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | ##########################################################################
3 | # This is the Cake bootstrapper script for Linux and OS X.
4 | # This file was downloaded from https://github.com/cake-build/resources
5 | # Feel free to change this file to fit your needs.
6 | ##########################################################################
7 |
8 | # Define directories.
9 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
10 | TOOLS_DIR=$SCRIPT_DIR/tools
11 | NUGET_EXE=$TOOLS_DIR/nuget.exe
12 | NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
13 | CAKE_VERSION=0.21.1
14 | CAKE_EXE=$TOOLS_DIR/Cake.$CAKE_VERSION/Cake.exe
15 |
16 | # Define default arguments.
17 | TARGET="Default"
18 | CONFIGURATION="Release"
19 | VERBOSITY="verbose"
20 | NUGETAPIKEY=
21 | DRYRUN=
22 | SCRIPT_ARGUMENTS=()
23 |
24 | # Parse arguments.
25 | for i in "$@"; do
26 | case $1 in
27 | -t|--target) TARGET="$2"; shift ;;
28 | -c|--configuration) CONFIGURATION="$2"; shift ;;
29 | -v|--verbosity) VERBOSITY="$2"; shift ;;
30 | -d|--dryrun) DRYRUN="-dryrun" ;;
31 | -n|--nuget) NUGETAPIKEY="$2"; shift ;;
32 | --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
33 | *) SCRIPT_ARGUMENTS+=("$1") ;;
34 | esac
35 | shift
36 | done
37 |
38 | # Make sure the tools folder exist.
39 | if [ ! -d "$TOOLS_DIR" ]; then
40 | mkdir "$TOOLS_DIR"
41 | fi
42 |
43 | ###########################################################################
44 | # INSTALL .NET CORE CLI
45 | ###########################################################################
46 |
47 | echo "Installing .NET CLI..."
48 | if [ ! -d "$SCRIPT_DIR/.dotnet" ]; then
49 | mkdir "$SCRIPT_DIR/.dotnet"
50 | fi
51 | curl -Lsfo "$SCRIPT_DIR/.dotnet/dotnet-install.sh" https://dot.net/v1/dotnet-install.sh
52 | sudo bash "$SCRIPT_DIR/.dotnet/dotnet-install.sh" --version 2.0.0 --install-dir .dotnet --no-path
53 | export PATH="$SCRIPT_DIR/.dotnet":$PATH
54 | export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
55 | export DOTNET_CLI_TELEMETRY_OPTOUT=1
56 | "$SCRIPT_DIR/.dotnet/dotnet" --info
57 |
58 | ###########################################################################
59 | # INSTALL NUGET
60 | ###########################################################################
61 |
62 | # Download NuGet if it does not exist.
63 | if [ ! -f "$NUGET_EXE" ]; then
64 | echo "Downloading NuGet..."
65 | curl -Lsfo "$NUGET_EXE" $NUGET_URL
66 | if [ $? -ne 0 ]; then
67 | echo "An error occured while downloading nuget.exe."
68 | exit 1
69 | fi
70 | fi
71 |
72 | ###########################################################################
73 | # INSTALL CAKE
74 | ###########################################################################
75 |
76 | if [ ! -f "$CAKE_EXE" ]; then
77 | mono "$NUGET_EXE" install Cake -Version $CAKE_VERSION -OutputDirectory "$TOOLS_DIR"
78 | if [ $? -ne 0 ]; then
79 | echo "An error occured while installing Cake."
80 | exit 1
81 | fi
82 | fi
83 |
84 | # Make sure that Cake has been installed.
85 | if [ ! -f "$CAKE_EXE" ]; then
86 | echo "Could not find Cake.exe at '$CAKE_EXE'."
87 | exit 1
88 | fi
89 |
90 | ###########################################################################
91 | # RUN BUILD SCRIPT
92 | ###########################################################################
93 |
94 | # Start Cake
95 | exec mono "$CAKE_EXE" build.cake --verbosity=$VERBOSITY --configuration=$CONFIGURATION --target=$TARGET --nuget=$NUGETAPIKEY $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
96 |
--------------------------------------------------------------------------------
/samples/Simple.HttpPatch.Samples/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Simple.HttpPatch.Samples.Model;
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | namespace Simple.HttpPatch.Samples.Controllers
7 | {
8 | [Route("api/[controller]")]
9 | public class ValuesController : Controller
10 | {
11 | private Person _person;
12 |
13 | public ValuesController()
14 | {
15 | _person = new Person(1, "Bob", 18, new Guid(), new DateTime(1992, 2, 14));
16 | }
17 |
18 | // GET api/values
19 | [HttpGet]
20 | public string Get()
21 | {
22 | return _person.ToString();
23 | }
24 |
25 | [HttpPatch]
26 | public IEnumerable Patch([FromBody] Patch person)
27 | {
28 | string original = _person.ToString();
29 |
30 | person.Apply(_person);
31 |
32 | return new[]
33 | {
34 | $"old: {original}",
35 | $"new: {_person}"
36 | };
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/samples/Simple.HttpPatch.Samples/Model/Person.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Simple.HttpPatch.Samples.Model
4 | {
5 | public record Person(int Id, string Name, int? Age, [property: PatchIgnore] Guid Guid, [property: PatchIgnoreNull] DateTime BirthDate)
6 | {
7 | public override string ToString() => $"Id: {Id}; Name: {Name}; Age: {Age}; Guid: {Guid}; BirthDate: {BirthDate};";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/Simple.HttpPatch.Samples/Program.cs:
--------------------------------------------------------------------------------
1 |
2 | using Simple.HttpPatch;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 |
6 | // Add services to the container.
7 |
8 | builder.Services.AddControllers(options =>
9 | {
10 | options.ModelBinderProviders.Insert(0, new PatchModelBinderProvider());
11 | });
12 |
13 | var app = builder.Build();
14 |
15 | app.UseHttpsRedirection();
16 |
17 | app.UseAuthorization();
18 |
19 | app.MapControllers();
20 |
21 | app.Run();
22 |
--------------------------------------------------------------------------------
/samples/Simple.HttpPatch.Samples/Simple.HttpPatch.Samples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | 1.1.0
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/DynamicMemberBinder.cs:
--------------------------------------------------------------------------------
1 | using System.Dynamic;
2 |
3 | namespace Simple.HttpPatch;
4 |
5 | public class DynamicMemberBinder : SetMemberBinder
6 | {
7 | public DynamicMemberBinder(string name, bool ignoreCase) : base(name, ignoreCase)
8 | {
9 | }
10 |
11 | public override DynamicMetaObject FallbackSetMember(DynamicMetaObject target, DynamicMetaObject value,
12 | DynamicMetaObject errorSuggestion)
13 | {
14 | throw new System.NotImplementedException();
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/Patch.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Dynamic;
5 | using System.Linq;
6 | using System.Reflection;
7 |
8 | namespace Simple.HttpPatch
9 | {
10 | public sealed class Patch : DynamicObject where TModel : class
11 | {
12 | private readonly IDictionary _changedProperties = new Dictionary();
13 |
14 | public override bool TrySetMember(SetMemberBinder binder, object value)
15 | {
16 | PropertyInfo propertyInfo = typeof(TModel).GetProperty(binder.Name,
17 | BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
18 | if (propertyInfo is not null)
19 | {
20 | var isIgnoredProperty = propertyInfo.GetCustomAttribute() is not null;
21 | if (!isIgnoredProperty)
22 | {
23 | var isIgnoredIfNull = propertyInfo.GetCustomAttribute() is not null;
24 | if (isIgnoredIfNull)
25 | {
26 | if (value is not null)
27 | {
28 | _changedProperties.Add(propertyInfo, value);
29 | }
30 | }
31 | else
32 | {
33 | _changedProperties.Add(propertyInfo, value);
34 | }
35 | }
36 | }
37 |
38 | return base.TrySetMember(binder, value);
39 | }
40 |
41 | public void Apply(TModel delta)
42 | {
43 | if (delta is null)
44 | {
45 | throw new ArgumentNullException(nameof(delta));
46 | }
47 |
48 | foreach (var property in _changedProperties)
49 | {
50 | if (!IsExcludedProperty(property.Key.Name))
51 | {
52 | object value = ChangeType(property.Value, property.Key.PropertyType);
53 | property.Key.SetValue(delta, value);
54 | }
55 | }
56 | }
57 |
58 | private static object ChangeType(object value, Type type)
59 | {
60 | try
61 | {
62 | if (type == typeof(Guid))
63 | {
64 | return Guid.Parse((string)value);
65 | }
66 |
67 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
68 | {
69 | if (value is null)
70 | {
71 | return null;
72 | }
73 |
74 | type = Nullable.GetUnderlyingType(type);
75 | }
76 |
77 | return Convert.ChangeType(value, type ?? throw new ArgumentNullException(nameof(type)));
78 | }
79 | catch
80 | {
81 | return null;
82 | }
83 | }
84 |
85 | private static bool IsExcludedProperty(string propertyName)
86 | {
87 | IEnumerable defaultExcludedProperties = new[] { "ID" };
88 | return defaultExcludedProperties.Contains(propertyName.ToUpper());
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/PatchCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Simple.HttpPatch
5 | {
6 | public class PatchCollection : List> where TModel : class, new()
7 | {
8 | public void Apply(IList deltas)
9 | {
10 | if (deltas is null)
11 | {
12 | throw new ArgumentNullException(nameof(deltas));
13 | }
14 |
15 | for (int i = 0; i < deltas.Count; i++)
16 | {
17 | this[i].Apply(deltas[i]);
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/PatchIgnoreAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Simple.HttpPatch
4 | {
5 | [AttributeUsage(AttributeTargets.Property)]
6 | public class PatchIgnoreAttribute : Attribute
7 | {
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/PatchIgnoreNullAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Simple.HttpPatch
4 | {
5 | [AttributeUsage(AttributeTargets.Property)]
6 | public class PatchIgnoreNullAttribute : Attribute
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/PatchModelBinderProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 | using System;
3 |
4 | namespace Simple.HttpPatch;
5 |
6 | public class PatchModelBinderProvider:IModelBinderProvider
7 | {
8 | public IModelBinder GetBinder(ModelBinderProviderContext context)
9 | {
10 | if (context.Metadata is { IsComplexType: true, ModelType.IsGenericType: true } &&
11 | context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(Patch<>))
12 | {
13 | var modelType = context.Metadata.ModelType.GenericTypeArguments[0];
14 | var binderType = typeof(PatchObjectModelBinder<>).MakeGenericType(modelType);
15 | return (IModelBinder)Activator.CreateInstance(binderType);
16 | }
17 |
18 | return null;
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/PatchObjectModelBinder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Formatters;
2 | using Microsoft.AspNetCore.Mvc.ModelBinding;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Dynamic;
6 | using System.IO;
7 | using System.Text.Json;
8 | using System.Threading.Tasks;
9 |
10 | namespace Simple.HttpPatch;
11 |
12 | public class PatchObjectModelBinder : IModelBinder where T : class
13 | {
14 | public async Task BindModelAsync(ModelBindingContext bindingContext)
15 | {
16 | if (bindingContext == null)
17 | {
18 | throw new ArgumentNullException(nameof(bindingContext));
19 | }
20 |
21 | using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
22 | {
23 | var json = await reader.ReadToEndAsync();
24 |
25 | dynamic dynamicObject = Activator.CreateInstance(typeof(Patch));
26 |
27 | using (JsonDocument doc = JsonDocument.Parse(json))
28 | {
29 | foreach (var property in doc.RootElement.EnumerateObject())
30 | {
31 | dynamicObject.TrySetMember(new DynamicMemberBinder(property.Name, true), property.Value.ToString());
32 | }
33 | }
34 |
35 | bindingContext.Result = ModelBindingResult.Success(dynamicObject);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Simple.HttpPatch/Simple.HttpPatch.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | Roman Marusyk
6 |
7 |
8 | https://github.com/Marusyk/Simple.HttpPatch
9 | patch; webapi; restful;
10 | Simple.HttpPatch implementation for .NET to easily allow & apply partial REST-ful service (through Web API)
11 | true
12 | 1.1.1-beta
13 | https://github.com/Marusyk/Simple.HttpPatch/blob/master/LICENSE
14 | https://github.com/Marusyk/Simple.HttpPatch/blob/master/HttpPatch.png?raw=true
15 | 10
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/Simple.HttpPatch.Tests/Simple.HttpPatch.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------