├── .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 | HttpPatch 3 |

4 | 5 | [![AppVeyor](https://ci.appveyor.com/api/projects/status/8sq80lyqcatsnssy?svg=true)](https://ci.appveyor.com/project/Marusyk/simple-httppatch) [![GitHub (pre-)release](https://img.shields.io/github/release/Marusyk/Simple.HttpPatch/all.svg)](https://github.com/Marusyk/Simple.HttpPatch/releases/tag/v1.0.0-beta) [![NuGet Pre Release](https://img.shields.io/nuget/vpre/Simple.HttpPatch.svg)](https://www.nuget.org/packages/Simple.HttpPatch) 6 | [![NuGet](https://img.shields.io/nuget/dt/Simple.HttpPatch.svg)]() 7 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) ![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat) 8 | 9 | # Simple.HttpPatch [![Stand With Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](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 | --------------------------------------------------------------------------------