Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: shkup/GraphDiff
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: zzzprojects/GraphDiff
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.

Commits on Dec 18, 2013

  1. VS2013, Nuget Package Restore enabled.

    Minor readme additions.
    b9chris committed Dec 18, 2013
    Copy the full SHA
    53a43a7 View commit details
  2. Enable Nuget Package Restore and EF Migrations in Tests project to ma…

    …ke it easier to pull and generate a test db.
    b9chris committed Dec 18, 2013
    Copy the full SHA
    b16dc3f View commit details
  3. Copy the full SHA
    4e8b538 View commit details
  4. Introduced a broken test for a Model whose subclass is attached to th…

    …e DbContext. GetKeysFor() calls GetFirstBaseType() naively without checking to see which class is actually tied to the DbContext (in fact, it's possible both are), and then asks the DbContext for the base class's DbSet - which in this case, there is none. That throws an Exception inside a yield IEnumerable, several methods deep in recursion - pretty hard for calling code to debug.
    b9chris committed Dec 18, 2013
    Copy the full SHA
    11cbd40 View commit details
  5. Copy the full SHA
    c0d80f7 View commit details
  6. Code cleanup.

    b9chris committed Dec 18, 2013
    Copy the full SHA
    114070c View commit details
  7. All tests passing.

    b9chris committed Dec 18, 2013
    Copy the full SHA
    48a5562 View commit details
  8. Copy the full SHA
    9787b86 View commit details

Commits on Dec 23, 2013

  1. Fix merge.

    b9chris committed Dec 23, 2013
    Copy the full SHA
    61f6015 View commit details

Commits on Jan 3, 2014

  1. Added bug fixes for inheritance and abstract classes.

    Added support for optimistic concurrency.
    refactorthis committed Jan 3, 2014
    Copy the full SHA
    8d6296e View commit details
  2. Copy the full SHA
    aa1588f View commit details
  3. fix zzzprojects#20

    refactorthis committed Jan 3, 2014
    Copy the full SHA
    ba7d876 View commit details
  4. Copy the full SHA
    c927b21 View commit details
  5. Copy the full SHA
    55a2648 View commit details
  6. Copy the full SHA
    c947de2 View commit details
  7. Cleaned up UpdateConfigurationVisitor

    andyp committed Jan 3, 2014
    Copy the full SHA
    73e551c View commit details
  8. Cleaned up tests a bit

    andyp committed Jan 3, 2014
    Copy the full SHA
    93ff6af View commit details
  9. Cleaned up DbContextExtensions

    andyp committed Jan 3, 2014
    Copy the full SHA
    8275cd7 View commit details
  10. - Merged PR

       - Added new test for new aggregate (more to come)
    
     - Fix for adding associated entity right before calling UpdateGraph.
     -Test class refactoring, getting ready for more comprehensive testing.
    refactorthis committed Jan 3, 2014
    Copy the full SHA
    72c5ea6 View commit details

Commits on Jan 4, 2014

  1. Copy the full SHA
    ec546f9 View commit details
  2. Updated some comments

    andyp committed Jan 4, 2014
    Copy the full SHA
    8a2406c View commit details
  3. Create method IsKeyIdentical()

    andyp committed Jan 4, 2014
    Copy the full SHA
    c4c5c4c View commit details
  4. Made cast implicit

    andyp committed Jan 4, 2014
    Copy the full SHA
    13f66be View commit details
  5. Rewrote AttachCyclicNavigationProperty, changed signature to take an …

    …IObjectContextAdapter
    andyp committed Jan 4, 2014
    Copy the full SHA
    3edeac1 View commit details
  6. Simplified logic in UpdateEntityRecursive

    andyp committed Jan 4, 2014
    Copy the full SHA
    414a1bc View commit details
  7. Inverted if

    andyp committed Jan 4, 2014
    Copy the full SHA
    d7cab7b View commit details
  8. Copy the full SHA
    c9f845c View commit details
  9. Rewrote GetKeysFor as GetPrimaryKeyFieldsFor

    andyp committed Jan 4, 2014
    Copy the full SHA
    051a3d4 View commit details
  10. Refactored FindEntityMatching

    andyp committed Jan 4, 2014
    Copy the full SHA
    77a075d View commit details
  11. Moved some methods to extensions region

    andyp committed Jan 4, 2014
    Copy the full SHA
    fbd498f View commit details
  12. Copy the full SHA
    3efb2e4 View commit details
  13. Copy the full SHA
    d746799 View commit details
  14. Rename

    andyp committed Jan 4, 2014
    Copy the full SHA
    94a1f50 View commit details
  15. Renamed namespace

    andyp committed Jan 4, 2014
    Copy the full SHA
    a2b58f6 View commit details
  16. Replaced overload with default parameter

    andyp committed Jan 4, 2014
    Copy the full SHA
    c597a9d View commit details
  17. Copy the full SHA
    984457b View commit details
  18. Copy the full SHA
    6ef986b View commit details
  19. Copy the full SHA
    e2abe56 View commit details
  20. inlined EntityFrameworkIncludeHelper in RootEntity

    moved FindMatchingEntity to RootEntity
    andyp committed Jan 4, 2014
    Copy the full SHA
    6d86cec View commit details
  21. Copy the full SHA
    901be41 View commit details
  22. Wrapped access to AMember.Accessor (mostly)

    andyp committed Jan 4, 2014
    Copy the full SHA
    b2b9135 View commit details
  23. Copy the full SHA
    3a1752b View commit details
  24. moved IsKeyIdentical to AEntityMember

    moved CreateHash/GetKeyFields to AMember
    andyp committed Jan 4, 2014
    Copy the full SHA
    3f77617 View commit details
  25. Copy the full SHA
    98b3f39 View commit details
  26. Refactored empty collection handling

    andyp committed Jan 4, 2014
    Copy the full SHA
    f1610e4 View commit details
  27. Extracted Add-, DeleteElement in collections

    andyp committed Jan 4, 2014
    Copy the full SHA
    a34e7d7 View commit details
  28. Extracted UpdateElement

    andyp committed Jan 4, 2014
    Copy the full SHA
    18f2d66 View commit details
  29. Removed classes for different collection types, as they made the hard…

    …er to read instead of easier
    andyp committed Jan 4, 2014
    Copy the full SHA
    2baf73f View commit details
  30. Final cleanup

    andyp committed Jan 4, 2014
    Copy the full SHA
    7bcbdee View commit details

Commits on Jan 5, 2014

  1. Updated README.md with new blog url.

    Updated min required version of EF is 6.0.1
    refactorthis committed Jan 5, 2014
    Copy the full SHA
    0f5c83d View commit details
Showing with 7,059 additions and 1,777 deletions.
  1. +2 −0 .github/FUNDING.yml
  2. +27 −0 .github/ISSUE_TEMPLATE.md
  3. +6 −0 .gitignore
  4. +6 −0 GraphDiff/.nuget/NuGet.Config
  5. BIN GraphDiff/.nuget/NuGet.exe
  6. +144 −0 GraphDiff/.nuget/NuGet.targets
  7. BIN GraphDiff/.vs/GraphDiff/v15/sqlite3/storage.ide
  8. +22 −0 GraphDiff/GraphDiff.NetStandard21/GraphDiff.NetStandard21.csproj
  9. +52 −0 GraphDiff/GraphDiff.Shared/DbContextExtensions.cs
  10. +24 −0 GraphDiff/GraphDiff.Shared/GraphDiff.Shared.projitems
  11. +13 −0 GraphDiff/GraphDiff.Shared/GraphDiff.Shared.shproj
  12. +0 −5 GraphDiff/{GraphDiff → GraphDiff.Shared}/GraphDiffConfiguration.cs
  13. +22 −11 GraphDiff/{GraphDiff → GraphDiff.Shared}/IUpdateConfiguration.cs
  14. +140 −0 GraphDiff/GraphDiff.Shared/Internal/ConfigurationVisitor.cs
  15. +86 −0 GraphDiff/GraphDiff.Shared/Internal/DebugExtensions.cs
  16. +76 −0 GraphDiff/GraphDiff.Shared/Internal/Extensions.cs
  17. +43 −0 GraphDiff/GraphDiff.Shared/Internal/Graph/AssociatedEntityGraphNode.cs
  18. +180 −0 GraphDiff/GraphDiff.Shared/Internal/Graph/CollectionGraphNode.cs
  19. +258 −0 GraphDiff/GraphDiff.Shared/Internal/Graph/GraphNode.cs
  20. +64 −0 GraphDiff/GraphDiff.Shared/Internal/Graph/OwnedEntityGraphNode.cs
  21. +103 −0 GraphDiff/GraphDiff.Shared/Internal/GraphDiffer.cs
  22. +22 −0 GraphDiff/GraphDiff.Tests.NetStandard21/GraphDiff.Tests.NetStandard21.csproj
  23. +23 −0 GraphDiff/GraphDiff.Tests.Shared/Bootstrapper.cs
  24. +30 −0 GraphDiff/GraphDiff.Tests.Shared/GraphDiff.Tests.Shared.projitems
  25. +13 −0 GraphDiff/GraphDiff.Tests.Shared/GraphDiff.Tests.Shared.shproj
  26. +298 −0 GraphDiff/GraphDiff.Tests.Shared/Models/TestModels.cs
  27. +24 −0 GraphDiff/GraphDiff.Tests.Shared/TestBase.cs
  28. +80 −0 GraphDiff/GraphDiff.Tests.Shared/TestDbContext.cs
  29. +200 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/AddAggregateBehaviours.cs
  30. +394 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/AssociatedCollectionBehaviours.cs
  31. +167 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/AssociatedEntityBehaviours.cs
  32. +24 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/AttachedBehaviours.cs
  33. +81 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/ConfigurationVisitorBehaviours.cs
  34. +22 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/ErrorHandlingBehaviours.cs
  35. +74 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/GuidKeyBehaviors.cs
  36. +114 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/MiscBehaviours.cs
  37. +108 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/OneMemberBehaviours.cs
  38. +94 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/OptimisticConcurrencyBehaviours.cs
  39. +413 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/OwnedCollectionBehaviours.cs
  40. +109 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/OwnedEntityBehaviours.cs
  41. +278 −0 GraphDiff/GraphDiff.Tests.Shared/Tests/ThirdTierBehaviours.cs
  42. +17 −17 GraphDiff/GraphDiff.Tests/App.config
  43. +76 −72 GraphDiff/GraphDiff.Tests/GraphDiff.Tests.csproj
  44. +0 −87 GraphDiff/GraphDiff.Tests/Models/Models.cs
  45. +0 −1 GraphDiff/GraphDiff.Tests/Properties/AssemblyInfo.cs
  46. +0 −31 GraphDiff/GraphDiff.Tests/TestDbContext.cs
  47. +0 −957 GraphDiff/GraphDiff.Tests/Tests/Tests.cs
  48. +3 −4 GraphDiff/GraphDiff.Tests/packages.config
  49. +67 −28 GraphDiff/GraphDiff.sln
  50. +0 −17 GraphDiff/GraphDiff/App.config
  51. +0 −324 GraphDiff/GraphDiff/DbContextExtensions.cs
  52. +77 −67 GraphDiff/GraphDiff/GraphDiff.csproj
  53. +5 −6 GraphDiff/GraphDiff/Properties/AssemblyInfo.cs
  54. +0 −133 GraphDiff/GraphDiff/UpdateConfigurationVisitor.cs
  55. +3 −4 GraphDiff/GraphDiff/packages.config
  56. +17 −6 GraphDiff/RefactorThis.GraphDiff.nuspec
  57. +51 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/My.cs
  58. +12 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/Program.cs
  59. +123 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/Request_LazyLoad.cs
  60. +142 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/Request_ManyNav.cs
  61. +97 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/Template.cs
  62. +16 −0 GraphDiff/Z.EntityFrameworkGraphDiff.labEF6/Z.EntityFrameworkGraphDiff.labEF6.csproj
  63. +68 −7 README.md
  64. +10 −0 docs/404.md
  65. +1 −0 docs/CNAME
  66. +11 −0 docs/_config.yml
  67. +20 −0 docs/_data/meta.csv
  68. +21 −0 docs/_includes/_global_variable.html
  69. +60 −0 docs/_includes/aside.html
  70. +1 −0 docs/_includes/infozzzprojects-email.html
  71. +3 −0 docs/_includes/layout-angle-begin.html
  72. +5 −0 docs/_includes/layout-angle-end.html
  73. +21 −0 docs/_includes/site-footer.html
  74. +19 −0 docs/_includes/site-header-nav-context.html
  75. +68 −0 docs/_includes/site-header-nav-md.html
  76. +51 −0 docs/_includes/site-header-nav-site.html
  77. +41 −0 docs/_includes/site-products.html
  78. +7 −0 docs/_includes/template-example.html
  79. +1 −0 docs/_includes/template-exception-cause.html
  80. +4 −0 docs/_includes/template-exception.html
  81. +1 −0 docs/_includes/template-execute-thrown.html
  82. +16 −0 docs/_includes/template-h1.html
  83. +3 −0 docs/_includes/under-construction.html
  84. +146 −0 docs/_layouts/default.html
  85. +41 −0 docs/_sass/card-layout-z1.scss
  86. +26 −0 docs/_sass/card-layout-z2.scss
  87. +98 −0 docs/_sass/console.scss
  88. +87 −0 docs/_sass/hero.scss
  89. +53 −0 docs/_sass/highlight.scss
  90. +6 −0 docs/_sass/highlight2.scss
  91. +131 −0 docs/_sass/page-index.scss
  92. +45 −0 docs/_sass/scroll-to-top.scss
  93. +10 −0 docs/_sass/section-faq.scss
  94. +39 −0 docs/_sass/site-footer.scss
  95. +45 −0 docs/_sass/site-header-nav-context.scss
  96. +36 −0 docs/_sass/site-header-nav-md.scss
  97. +86 −0 docs/_sass/site-header-nav-site.scss
  98. +22 −0 docs/_sass/site-header.scss
  99. +24 −0 docs/_sass/site-products.scss
  100. +284 −0 docs/css/master.scss
  101. BIN docs/images/arrow-down1.png
  102. BIN docs/images/logo.png
  103. BIN docs/images/logo256X256-opacity.png
  104. BIN docs/images/logo256X256.png
  105. +156 −0 docs/index.md
  106. +3 −0 docs/pages/api/api.md
  107. +13 −0 docs/pages/faq/faq-general.md
  108. +12 −0 docs/pages/faq/faq-installation.md
  109. +9 −0 docs/pages/faq/faq.md
  110. +13 −0 docs/pages/faq/issue-tracker.md
  111. +5 −0 docs/pages/problems/problems.md
  112. +69 −0 docs/pages/site/contact-us.md
  113. +61 −0 docs/pages/site/download.md
  114. +52 −0 docs/pages/tutorials.md
  115. +53 −0 docs/pages/tutorials/detach-aggregated-entity.md
  116. +103 −0 docs/pages/tutorials/detach-associated-entity.md
  117. +170 −0 docs/pages/tutorials/detach-owned-entity.md
  118. +53 −0 docs/pages/tutorials/detach-single-entity.md
  119. +21 −0 docs/pages/tutorials/installing.md
  120. +60 −0 docs/pages/tutorials/overview.md
  121. +18 −0 docs/pages/tutorials/requirements.md
  122. +29 −0 docs/pages/tutorials/upgrading.md
  123. +6 −0 docs/robots.txt
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: [zzzprojects]
custom: ["https://zzzprojects.com/contribute"]
27 changes: 27 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Here is what to include in your request to make sure we implement a solution as quickly as possible.

## 1. Description
Describe the issue or propose a feature.

## 2. Exception
If you are seeing an exception, include the full exception details (message and stack trace).

```
Exception message:
Stack trace:
```

## 3. Fiddle or Project
If you are able,

Provide a Fiddle that reproduce the issue: https://dotnetfiddle.net/25Vjsn

Or provide a project/solution that we can run to reproduce the issue.
- Make sure the project compile
- Make sure to provide only the code that is required to reproduce the issue, not the whole project
- You can send private code here: info@zzzprojects.com

Otherwise, make sure to include as much information as possible to help our team to reproduce the issue.

## 4. Any further technical details
Add any relevant detail that can help us.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
*.pfx
*.snk
Z.Lab/
.vs/

# Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
[Bb]in/
[Oo]bj/
@@ -106,3 +111,4 @@ Generated_Code #added for RIA/Silverlight projects
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
GraphDiff/.vs/GraphDiff/v15/sqlite3/storage.ide
6 changes: 6 additions & 0 deletions GraphDiff/.nuget/NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
</configuration>
Binary file added GraphDiff/.nuget/NuGet.exe
Binary file not shown.
144 changes: 144 additions & 0 deletions GraphDiff/.nuget/NuGet.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>

<!-- Enable the restore command to run before builds -->
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>

<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>

<!-- Determines if package restore consent is required to restore packages -->
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>

<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>

<ItemGroup Condition=" '$(PackageSources)' == '' ">
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
<!-- The official NuGet package source (https://www.nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
<!--
<PackageSource Include="https://www.nuget.org/api/v2/" />
<PackageSource Include="https://my-nuget-source/nuget/" />
-->
</ItemGroup>

<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
</PropertyGroup>

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
</PropertyGroup>

<PropertyGroup>
<PackagesProjectConfig Condition=" '$(OS)' == 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config</PackagesProjectConfig>
<PackagesProjectConfig Condition=" '$(OS)' != 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config</PackagesProjectConfig>
</PropertyGroup>

<PropertyGroup>
<PackagesConfig Condition="Exists('$(MSBuildProjectDirectory)\packages.config')">$(MSBuildProjectDirectory)\packages.config</PackagesConfig>
<PackagesConfig Condition="Exists('$(PackagesProjectConfig)')">$(PackagesProjectConfig)</PackagesConfig>
</PropertyGroup>

<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>

<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 "$(NuGetExePath)"</NuGetCommand>

<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>

<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>

<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>

<!-- Commands -->
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>

<!-- We need to ensure packages are restored prior to assembly resolve -->
<BuildDependsOn Condition="$(RestorePackages) == 'true'">
RestorePackages;
$(BuildDependsOn);
</BuildDependsOn>

<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>

<Target Name="CheckPrerequisites">
<!-- Raise an error if we're unable to locate nuget.exe -->
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
<!--
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
This effectively acts as a lock that makes sure that the download operation will only happen once and all
parallel builds will have to wait for it to complete.
-->
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT;DownloadNuGetExe=$(DownloadNuGetExe)" />
</Target>

<Target Name="_DownloadNuGet">
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
</Target>

<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(RestoreCommand)"
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />

<Exec Command="$(RestoreCommand)"
LogStandardErrorAsError="true"
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
</Target>

<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />

<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>

<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputFilename ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Net" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
OutputFilename = Path.GetFullPath(OutputFilename);
Log.LogMessage("Downloading latest version of NuGet.exe...");
WebClient webClient = new WebClient();
webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename);
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
Binary file added GraphDiff/.vs/GraphDiff/v15/sqlite3/storage.ide
Binary file not shown.
22 changes: 22 additions & 0 deletions GraphDiff/GraphDiff.NetStandard21/GraphDiff.NetStandard21.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>RefactorThis.GraphDiff</AssemblyName>
<RootNamespace>RefactorThis.GraphDiff</RootNamespace>
<Version>3.1.3</Version>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>zzzproject.pfx</AssemblyOriginatorKeyFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>\GraphDiff.NetStandard21\RefactorThis.GraphDiff.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="EntityFramework" Version="6.3.0" />
</ItemGroup>

<Import Project="..\GraphDiff.Shared\GraphDiff.Shared.projitems" Label="Shared" />

</Project>
52 changes: 52 additions & 0 deletions GraphDiff/GraphDiff.Shared/DbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* This code is provided as is with no warranty. If you find a bug please report it on github.
* If you would like to use the code please leave this comment at the top of the page
* License MIT (c) Brent McKendrick 2012
*/

using System;
using System.Data.Entity;
using System.Linq.Expressions;
using RefactorThis.GraphDiff.Internal;
using RefactorThis.GraphDiff.Internal.Graph;

namespace RefactorThis.GraphDiff
{
public static class DbContextExtensions
{
/// <summary>
/// Merges a graph of entities with the data store.
/// </summary>
/// <typeparam name="T">The type of the root entity</typeparam>
/// <param name="context">The database context to attach / detach.</param>
/// <param name="entity">The root entity.</param>
/// <param name="mapping">The mapping configuration to define the bounds of the graph</param>
/// <param name="allowDelete">Allow to delete from graph.</param>
/// <returns>The attached entity graph</returns>
public static T UpdateGraph<T>(this DbContext context, T entity,
Expression<Func<IUpdateConfiguration<T>, object>> mapping = null, bool allowDelete = true)
where T : class, new()
{
var root = mapping == null ? new GraphNode() : new ConfigurationVisitor<T>().GetNodes(mapping);
root.AllowDelete = allowDelete;
var graphDiffer = new GraphDiffer<T>(root);
return graphDiffer.Merge(context, entity);
}

/// <summary>
/// Merges a graph of entities with the data store.
/// </summary>
/// <typeparam name="T">The type of the root entity</typeparam>
/// <param name="context">The database context to attach / detach.</param>
/// <param name="entity">The root entity.</param>
/// <param name="allowDelete">Allow to delete from graph.</param>
/// <returns>The attached entity graph</returns>
public static T UpdateGraph<T>(this DbContext context, T entity, bool allowDelete)
where T : class, new()
{
return context.UpdateGraph(entity, null, allowDelete);
}

// TODO add IEnumerable<T> entities
}
}
24 changes: 24 additions & 0 deletions GraphDiff/GraphDiff.Shared/GraphDiff.Shared.projitems
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>c97b2c2c-b005-438b-8d0c-ee3557d03041</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>GraphDiff.Shared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)DbContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GraphDiffConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\ConfigurationVisitor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\DebugExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Extensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\GraphDiffer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Graph\AssociatedEntityGraphNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Graph\CollectionGraphNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Graph\GraphNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Graph\OwnedEntityGraphNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IUpdateConfiguration.cs" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions GraphDiff/GraphDiff.Shared/GraphDiff.Shared.shproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>c97b2c2c-b005-438b-8d0c-ee3557d03041</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="GraphDiff.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -4,11 +4,6 @@
* License MIT (c) Brent McKendrick 2012
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RefactorThis.GraphDiff
{
/// <summary>
Original file line number Diff line number Diff line change
@@ -10,18 +10,16 @@

namespace RefactorThis.GraphDiff
{
/* Configuration and Mapping of update */

/// <summary>
/// Mapping configuration for an update graph
/// Mapping configuration for a merge graph
/// </summary>
/// <typeparam name="T">The type of the parent enity</typeparam>
/// <typeparam name="T">The type of the parent entity</typeparam>
public interface IUpdateConfiguration<T> { }

public static class UpdateConfigurationExtensions
{
/// <summary>
/// States that the child entity is a part of the aggregate and will be updated, added removed if changed in the parent's
/// States that the child entity is a part of the aggregate and will be updated, added or removed if changed in the parent's
/// navigational property.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
@@ -35,7 +33,7 @@ public static IUpdateConfiguration<T> OwnedEntity<T, T2>(this IUpdateConfigurati
}

/// <summary>
/// States that the child entity is not a part of the aggregate. The parent's navigation property will be updated, but entity changes to the
/// States that the child entity is not a part of the aggregate. The parent's navigation property will be updated, but changes to the
/// child will not be saved.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
@@ -49,7 +47,7 @@ public static IUpdateConfiguration<T> AssociatedEntity<T, T2>(this IUpdateConfig
}

/// <summary>
/// States that the child entity is a part of the aggregate and will be updated, added removed if changed in the parent's
/// States that the child entity is a part of the aggregate and will be updated, added or removed if changed in the parent's
/// navigational property.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
@@ -63,11 +61,9 @@ public static IUpdateConfiguration<T> OwnedEntity<T, T2>(this IUpdateConfigurati
return config;
}

/* Collection configuration */

/// <summary>
/// States that the child collection is a part of the aggregate and the entities inside will be updated,
/// added removed if changed in the parent's navigational property.
/// added or removed if changed in the parent's navigational property.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
/// <typeparam name="T2">The child collection type </typeparam>
@@ -93,9 +89,24 @@ public static IUpdateConfiguration<T> AssociatedCollection<T, T2>(this IUpdateCo
return config;
}

/// <summary>
/// States that the child collection is not a part of the aggregate. The parent's navigation property will be updated, but entity changes to the
/// child entities will not be saved.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
/// <typeparam name="T2">The child entity type </typeparam>
/// <param name="config">The configuration mapping</param>
/// <param name="expression">An expression specifying the child entity</param>
/// <param name="navExpression">An navigation expression specifying the parent entity</param>
/// <returns>Updated configuration mapping</returns>
public static IUpdateConfiguration<T> AssociatedCollection<T, T2>(this IUpdateConfiguration<T> config, Expression<Func<T, ICollection<T2>>> expression, Expression<Func<T2, T>> navExpression)
{
return config;
}

/// <summary>
/// States that the child collection is a part of the aggregate and the entities inside will be updated,
/// added removed if changed in the parent's navigational property.
/// added or removed if changed in the parent's navigational property.
/// </summary>
/// <typeparam name="T">The parent entity type</typeparam>
/// <typeparam name="T2">The child collection type </typeparam>
140 changes: 140 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/ConfigurationVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using RefactorThis.GraphDiff.Internal.Graph;

namespace RefactorThis.GraphDiff.Internal
{
/// <summary>
/// Reads an IUpdateConfiguration mapping and produces an GraphNode graph.
/// </summary>
/// <typeparam name="T"></typeparam>
internal class ConfigurationVisitor<T> : ExpressionVisitor
{
private GraphNode _currentMember;
private string _currentMethod = "";
private bool _isAssociatedCollectionWithNav;

public GraphNode GetNodes(Expression<Func<IUpdateConfiguration<T>, object>> expression)
{
var initialNode = new GraphNode();
_currentMember = initialNode;
Visit(expression);
return initialNode;
}

protected override Expression VisitMember(MemberExpression memberExpression)
{
var accessor = GetMemberAccessor(memberExpression);

if (_isAssociatedCollectionWithNav)
{
_currentMember.AccessorCyclicNavigationProperty = accessor;
}
else
{
var newMember = CreateNewMember(accessor);

_currentMember.Members.Push(newMember);
_currentMember = newMember;
}

return base.VisitMember(memberExpression);
}

protected override Expression VisitMethodCall(MethodCallExpression expression)
{
_currentMethod = expression.Method.Name;

if (_currentMethod.Equals("AssociatedCollection") && expression.Arguments.Count == 3)
{
Visit(expression.Arguments[1]);

try
{
_isAssociatedCollectionWithNav = true;
Visit(expression.Arguments[2]);
}
catch
{
throw;
}
finally
{
_isAssociatedCollectionWithNav = false;
}
}
else
{
// go left to right in the subtree (ignore first argument for now)
for (int i = 1; i < expression.Arguments.Count; i++)
{
Visit(expression.Arguments[i]);
}
}

// go back up the tree and continue
_currentMember = _currentMember.Parent;
return Visit(expression.Arguments[0]);
}

private GraphNode CreateNewMember(PropertyInfo accessor)
{
GraphNode newMember;
switch (_currentMethod)
{
case "OwnedEntity":
newMember = new OwnedEntityGraphNode(_currentMember, accessor);
break;
case "AssociatedEntity":
newMember = new AssociatedEntityGraphNode(_currentMember, accessor);
break;
case "OwnedCollection":
newMember = new CollectionGraphNode(_currentMember, accessor, true);
break;
case "AssociatedCollection":
newMember = new CollectionGraphNode(_currentMember, accessor, false);
break;
default:
throw new NotSupportedException("The method used in the update mapping is not supported");
}
return newMember;
}

private static PropertyInfo GetMemberAccessor(MemberExpression memberExpression)
{
PropertyInfo accessor = null;
var expression = memberExpression.Expression;
var constantExpression = expression as ConstantExpression;

if (constantExpression != null)
{
var container = constantExpression.Value;
var member = memberExpression.Member;

var fieldInfo = member as FieldInfo;
if (fieldInfo != null)
{
dynamic value = fieldInfo.GetValue(container);
accessor = (PropertyInfo) value.Body.Member;
}

var info = member as PropertyInfo;
if (info != null)
{
dynamic value = info.GetValue(container, null);
accessor = (PropertyInfo) value.Body.Member;
}
}
else
{
accessor = (PropertyInfo) memberExpression.Member;
}

if (accessor == null)
throw new NotSupportedException("Unknown accessor type found!");

return accessor;
}
}
}
86 changes: 86 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/DebugExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;

namespace RefactorThis.GraphDiff.Internal
{
public static class DebugExtensions
{
public static string DumpTrackedEntities(this DbContext context)
{
var trackedEntities = context
.ChangeTracker
.Entries()
.Where(t => t.State != EntityState.Detached && t.Entity != null)
.Select(t => new
{
t.State,
EntityType = t.Entity.GetType(),
Original = t.State != EntityState.Added
? t.OriginalValues.PropertyNames.ToDictionary(pn => pn, pn => t.OriginalValues[pn])
: new Dictionary<string, object>(),
Current = t.State != EntityState.Deleted
? t.CurrentValues.PropertyNames.ToDictionary(pn => pn, pn => t.CurrentValues[pn])
: new Dictionary<string, object>(),
HashCode = t.Entity.GetHashCode()
})
.OrderBy(e => e.State).ThenBy(e => e.EntityType.Name)
.ToList();

var builder = new StringBuilder();

EntityState? previousState = null;
foreach (var entity in trackedEntities)
{
if (entity.State != previousState)
{
if (builder.Length > 0)
{
builder.AppendLine();
}

builder.AppendLine(entity.State.ToString());
builder.AppendLine("----------");
}
previousState = entity.State;

builder.AppendFormat("{0} (# {1})", entity.EntityType.Name, entity.HashCode).AppendLine();

bool outerIsOriginal = entity.Original.Count >= entity.Current.Count;
Dictionary<string, object> outer = outerIsOriginal ? entity.Original : entity.Current;
Dictionary<string, object> inner = outerIsOriginal ? entity.Current : entity.Original;

var propertyValues = from fd in outer
join pd in inner on fd.Key equals pd.Key into joinedT
from pd in joinedT.DefaultIfEmpty()
select new
{
fd.Key,
OriginalValue = outerIsOriginal ? fd.Value : pd.Value,
CurrentValue = outerIsOriginal ? pd.Value : fd.Value
};

foreach (var propertyValue in propertyValues)
{
switch (entity.State)
{
case EntityState.Added:
builder.AppendFormat(" {0}: '{1}'", propertyValue.Key, propertyValue.CurrentValue).AppendLine();
break;
case EntityState.Deleted:
builder.AppendFormat(" {0}: '{1}'", propertyValue.Key, propertyValue.OriginalValue).AppendLine();
break;
default:
builder.AppendFormat(" {0}: changed from '{1}' to '{2}'", propertyValue.Key, propertyValue.OriginalValue, propertyValue.CurrentValue).AppendLine();
break;
}
}

builder.AppendLine();
}

return builder.ToString();
}
}
}
76 changes: 76 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal
{
internal static class Extensions
{
internal static IEnumerable<PropertyInfo> GetPrimaryKeyFieldsFor(this IObjectContextAdapter context, Type entityType)
{
var metadata = context.ObjectContext.MetadataWorkspace
.GetEntityTypeByType(entityType);

if (metadata == null)
{
throw new InvalidOperationException(String.Format("The type {0} is not known to the DbContext.", entityType.FullName));
}

return metadata.KeyMembers.Select(k => entityType.GetProperty(k.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)).ToList();
}

internal static IEnumerable<NavigationProperty> GetRequiredNavigationPropertiesForType(this IObjectContextAdapter context, Type entityType)
{
return context.GetNavigationPropertiesForType(ObjectContext.GetObjectType(entityType))
.Where(navigationProperty => navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One);
}

internal static IEnumerable<NavigationProperty> GetNavigationPropertiesForType(this IObjectContextAdapter context, Type entityType)
{

return context.ObjectContext.MetadataWorkspace.GetEntityTypeByType(entityType).NavigationProperties;
}

internal static string GetEntitySetName(this IObjectContextAdapter context, Type entityType)
{
Type type = entityType;
EntitySetBase set = null;

while (set == null && type != null)
{
set = context.ObjectContext.MetadataWorkspace
.GetEntityContainer(context.ObjectContext.DefaultContainerName, DataSpace.CSpace)
.EntitySets
.FirstOrDefault(item => item.ElementType.Name.Equals(type.Name));

type = type.BaseType;
}

return set != null ? set.Name : null;
}

/// <summary>A MetadataWorkspace extension method that gets entity type by type.</summary>
/// <param name="metadataWorkspace">The metadataWorkspace to act on.</param>
/// <param name="entityType">Type of the entity.</param>
/// <returns>The entity type by type.</returns>
/// not support class in generic class
internal static EntityType GetEntityTypeByType(this MetadataWorkspace metadataWorkspace, Type entityType)
{
string name = ObjectContext.GetObjectType(entityType).FullName.Replace("+", ".");
var lenght = name.IndexOf("`");

if (lenght != -1)
{
name = name.Substring(0, lenght);
}

return metadataWorkspace
.GetItems<EntityType>(DataSpace.OSpace)
.SingleOrDefault(p => p.FullName == name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Data.Entity;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal.Graph
{
internal class AssociatedEntityGraphNode : GraphNode
{
internal AssociatedEntityGraphNode(GraphNode parent, PropertyInfo accessor)
: base(parent, accessor)
{
}

public override void Update<T>(DbContext context, T persisted, T updating)
{
var dbValue = GetValue<object>(persisted);
var newValue = GetValue<object>(updating);

if (newValue == null)
{
SetValue(persisted, null);
return;
}

// do nothing if the key is already identical
if (IsKeyIdentical(context, newValue, dbValue))
{
return;
}

newValue = AttachAndReloadAssociatedEntity(context, newValue);

SetValue(persisted, newValue);
}

protected override IEnumerable<string> GetRequiredNavigationPropertyIncludes(DbContext context)
{
return Accessor != null
? GetRequiredNavigationPropertyIncludes(context, Accessor.PropertyType, IncludeString)
: new string[0];
}
}
}
180 changes: 180 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/Graph/CollectionGraphNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Linq;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal.Graph
{
internal class CollectionGraphNode : GraphNode
{
private readonly bool _isOwned;

internal CollectionGraphNode(GraphNode parent, PropertyInfo accessor, bool isOwned)
: base(parent, accessor)
{
_isOwned = isOwned;
}

public override void Update<T>(DbContext context, T existing, T entity)
{
var innerElementType = GetCollectionElementType();
var updateValues = GetValue<IEnumerable>(entity) ?? new List<object>();
var dbCollection = GetValue<IEnumerable>(existing) ?? CreateMissingCollection(existing, innerElementType);

var dbHash = dbCollection.Cast<object>().ToDictionary(item => CreateEntityKey(context, item));

// Iterate through the elements from the updated graph and try to match them against the db graph
var updateList = updateValues.OfType<object>().ToList();
for (int i = 0; i < updateList.Count; i++)
{
var updateItem = updateList[i];
var key = CreateEntityKey(context, updateItem);

// try to find item with same key in db collection
object dbItem;
if (dbHash.TryGetValue(key, out dbItem))
{
UpdateElement(context, existing, updateItem, dbItem);
dbHash.Remove(key);
}
else
{
updateList[i] = AddElement(context, existing, updateItem, dbCollection);
}
}

if (CheckAllowDelete(this))
{
// remove obsolete items
foreach (var dbItem in dbHash.Values)
{
RemoveElement(context, dbItem, dbCollection);
}
}
}

private static bool CheckAllowDelete(GraphNode node)
{
if (node.AllowDelete.HasValue)
{
return node.AllowDelete.Value;
}
else if (node.Parent != null)
{
return CheckAllowDelete(node.Parent);
}
else
{
return true;
}
}

private object AddElement<T>(DbContext context, T existing, object updateItem, object dbCollection)
{
if (!_isOwned)
{
updateItem = AttachAndReloadAssociatedEntity(context, updateItem);
}
else if (context.Entry(updateItem).State == EntityState.Detached)
{
var entityType = ObjectContext.GetObjectType(updateItem.GetType());
var instance = CreateEmptyEntityWithKey(context, updateItem);

if (!(bool) dbCollection.GetType().GetMethod("Contains").Invoke(dbCollection, new[] {updateItem}))
{
context.Set(entityType).Add(instance);
context.Entry(instance).CurrentValues.SetValues(updateItem);

foreach (var childMember in Members)
{
childMember.Update(context, instance, updateItem);
}
}

updateItem = instance;
}

if (!(bool)dbCollection.GetType().GetMethod("Contains").Invoke(dbCollection, new[] { updateItem }))
{
dbCollection.GetType().GetMethod("Add").Invoke(dbCollection, new[] { updateItem });
}

AttachCyclicNavigationProperty(context, existing, updateItem, AccessorCyclicNavigationProperty);

return updateItem;
}

private void UpdateElement<T>(DbContext context, T existing, object updateItem, object dbItem)
{
if (!_isOwned) return;

UpdateValuesWithConcurrencyCheck(context, updateItem, dbItem);

AttachCyclicNavigationProperty(context, existing, updateItem);

foreach (var childMember in Members)
{
childMember.Update(context, dbItem, updateItem);
}
}

private void RemoveElement(DbContext context, object dbItem, object dbCollection)
{
dbCollection.GetType().GetMethod("Remove").Invoke(dbCollection, new[] { dbItem });

AttachRequiredNavigationProperties(context, dbItem, dbItem);

if (_isOwned)
{
context.Set(ObjectContext.GetObjectType(dbItem.GetType())).Remove(dbItem);
}
}

private IEnumerable CreateMissingCollection(object existing, Type elementType)
{
var collectionType = !Accessor.PropertyType.IsInterface ? Accessor.PropertyType : typeof(List<>).MakeGenericType(elementType);
var collection = (IEnumerable)Activator.CreateInstance(collectionType);
SetValue(existing, collection);
return collection;
}

protected override IEnumerable<string> GetRequiredNavigationPropertyIncludes(DbContext context)
{
if (_isOwned)
{
return base.GetRequiredNavigationPropertyIncludes(context);
}

return Accessor != null
? GetRequiredNavigationPropertyIncludes(context, GetCollectionElementType(), IncludeString)
: new string[0];
}

private Type GetCollectionElementType()
{
return GetCollectionElementType(Accessor.PropertyType);
}

// Z.EntityFramework.Plus\shared\Z.EF.Plus.QueryIncludeOptimized.Shared\QueryIncludeOptimizedNullCollection.cs + Keep Array logique.
private static Type GetCollectionElementType(Type propertyType)
{
if (propertyType.IsArray)
{
return propertyType.GetElementType();
}
else if (propertyType.GetGenericArguments().Length == 1 && typeof(IEnumerable).IsAssignableFrom(propertyType))
{
return propertyType.GetGenericArguments()[0];
}
else if (propertyType.BaseType != null && propertyType.BaseType != typeof(object))
{
return GetCollectionElementType(propertyType.BaseType);
}

throw new InvalidOperationException("GraphDiff requires the collection to be either IEnumerable<T> or T[]");
}
}
}
258 changes: 258 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/Graph/GraphNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal.Graph
{
internal class GraphNode
{
#region Fields, Properties Constructors

public GraphNode Parent { get; private set; }
public Stack<GraphNode> Members { get; private set; }
public bool? AllowDelete { get; set; }

protected readonly PropertyInfo Accessor;

internal PropertyInfo AccessorCyclicNavigationProperty;

protected string IncludeString
{
get
{
var ownIncludeString = Accessor != null ? Accessor.Name : null;
return Parent != null && Parent.IncludeString != null
? Parent.IncludeString + "." + ownIncludeString
: ownIncludeString;
}
}

public GraphNode()
{
Members = new Stack<GraphNode>();
}

protected GraphNode(GraphNode parent, PropertyInfo accessor)
{
Accessor = accessor;
Members = new Stack<GraphNode>();
Parent = parent;
}

#endregion

// overridden by different implementations
public virtual void Update<T>(DbContext context, T persisted, T updating) where T : class, new()
{
UpdateValuesWithConcurrencyCheck(context, updating, persisted);

// Foreach branch perform recursive update
foreach (var member in Members)
{
member.Update(context, persisted, updating);
}
}

protected T GetValue<T>(object instance)
{
return (T)Accessor.GetValue(instance, null);
}

protected void SetValue(object instance, object value)
{
Accessor.SetValue(instance, value, null);
}

protected static EntityKey CreateEntityKey(IObjectContextAdapter context, object entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

return context.ObjectContext.CreateEntityKey(context.GetEntitySetName(ObjectContext.GetObjectType(entity.GetType())), entity);
}

internal void GetIncludeStrings(DbContext context, List<string> includeStrings)
{
var ownIncludeString = IncludeString;
if (!string.IsNullOrEmpty(ownIncludeString))
{
includeStrings.Add(ownIncludeString);
}

includeStrings.AddRange(GetRequiredNavigationPropertyIncludes(context));

foreach (var member in Members)
{
member.GetIncludeStrings(context, includeStrings);
}
}

protected virtual IEnumerable<string> GetRequiredNavigationPropertyIncludes(DbContext context)
{
return new string[0];
}

protected static IEnumerable<string> GetRequiredNavigationPropertyIncludes(DbContext context, Type entityType, string ownIncludeString)
{
return context.GetRequiredNavigationPropertiesForType(entityType)
.Select(navigationProperty => ownIncludeString + "." + navigationProperty.Name);
}

protected static void AttachCyclicNavigationProperty(IObjectContextAdapter context, object parent, object child, PropertyInfo parentNavigationProperty = null)
{
if (parent == null || child == null) return;

var parentType = ObjectContext.GetObjectType(parent.GetType());
var childType = ObjectContext.GetObjectType(child.GetType());

var navigationProperties = context.GetNavigationPropertiesForType(childType);

if (parentNavigationProperty == null)
{
// IF not parent property is specified, we take the first one if we found something
parentNavigationProperty = navigationProperties
.Where(navigation => navigation.TypeUsage.EdmType.Name == parentType.Name)
.Select(navigation => childType.GetProperty(navigation.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
.FirstOrDefault();
}

if (parentNavigationProperty != null)
parentNavigationProperty.SetValue(child, parent, null);
}

protected static void UpdateValuesWithConcurrencyCheck<T>(DbContext context, T from, T to) where T : class
{
if (context.Entry(to).State != EntityState.Added)
{
EnsureConcurrency(context, from, to);
}

context.Entry(to).CurrentValues.SetValues(from);
}

protected static object AttachAndReloadAssociatedEntity(DbContext context, object entity)
{
var localCopy = FindLocalByKey(context, entity);
if (localCopy != null) return localCopy;

if (context.Entry(entity).State == EntityState.Detached)
{
var entityType = ObjectContext.GetObjectType(entity.GetType());
var instance = CreateEmptyEntityWithKey(context, entity);

context.Set(entityType).Attach(instance);
context.Entry(instance).Reload();

AttachRequiredNavigationProperties(context, entity, instance);
return instance;
}

if (GraphDiffConfiguration.ReloadAssociatedEntitiesWhenAttached)
{
context.Entry(entity).Reload();
}

return entity;
}

private static object FindLocalByKey(DbContext context, object entity)
{
var eType = ObjectContext.GetObjectType(entity.GetType());
return context.Set(eType).Local.OfType<object>().FirstOrDefault(local => IsKeyIdentical(context, local, entity));
}

protected static void AttachRequiredNavigationProperties(DbContext context, object updating, object persisted)
{
var entityType = ObjectContext.GetObjectType(updating.GetType());
foreach (var navigationProperty in context.GetRequiredNavigationPropertiesForType(updating.GetType()))
{
var navigationPropertyInfo = entityType.GetProperty(navigationProperty.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

var associatedEntity = navigationPropertyInfo.GetValue(updating, null);
if (associatedEntity != null)
{
associatedEntity = FindEntityByKey(context, associatedEntity);
}

navigationPropertyInfo.SetValue(persisted, associatedEntity, null);
}
}

private static object FindEntityByKey(DbContext context, object associatedEntity)
{
var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType());
var keyFields = context.GetPrimaryKeyFieldsFor(associatedEntityType);
var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray();
return context.Set(associatedEntityType).Find(keys);
}

protected static object CreateEmptyEntityWithKey(IObjectContextAdapter context, object entity)
{
var instance = Activator.CreateInstance(ObjectContext.GetObjectType(entity.GetType()));
CopyPrimaryKeyFields(context, entity, instance);
return instance;
}

private static void CopyPrimaryKeyFields(IObjectContextAdapter context, object from, object to)
{
var keyProperties = context.GetPrimaryKeyFieldsFor(from.GetType()).ToList();
foreach (var keyProperty in keyProperties)
keyProperty.SetValue(to, keyProperty.GetValue(from, null), null);
}

protected static bool IsKeyIdentical(DbContext context, object newValue, object dbValue)
{
if (newValue == null || dbValue == null) return false;

return CreateEntityKey(context, newValue) == CreateEntityKey(context, dbValue);
}

private static void EnsureConcurrency<T>(IObjectContextAdapter db, T entity1, T entity2)
{
// get concurrency properties of T
var entityType = ObjectContext.GetObjectType(entity1.GetType());
var metadata = db.ObjectContext.MetadataWorkspace;

var objType = metadata.GetEntityTypeByType(entityType);

// need internal string, code smells bad.. any better way to do this?
var cTypeName = (string)objType.GetType()
.GetProperty("CSpaceTypeName", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(objType, null);

var conceptualType = metadata.GetItems<EntityType>(DataSpace.CSpace).Single(p => p.FullName == cTypeName);
var concurrencyProperties = conceptualType.Members
.Where(member => member.TypeUsage.Facets.Any(facet => facet.Name == "ConcurrencyMode" && (ConcurrencyMode)facet.Value == ConcurrencyMode.Fixed))
.Select(member => entityType.GetProperty(member.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
.ToList();

// Check if concurrency properties are equal
// TODO EF should do this automatically should it not?
foreach (var concurrencyProp in concurrencyProperties)
{
// if is byte[] use array comparison, else equals().

var type = concurrencyProp.PropertyType;
var obj1 = concurrencyProp.GetValue(entity1, null);
var obj2 = concurrencyProp.GetValue(entity2, null);

if (
(obj1 == null || obj2 == null) ||
(type == typeof (byte[]) && !((byte[]) obj1).SequenceEqual((byte[]) obj2)) ||
(type != typeof (byte[]) && !obj1.Equals(obj2))
)
{
throw new DbUpdateConcurrencyException(String.Format("{0} failed optimistic concurrency", concurrencyProp.Name));
}
}
}
}
}
64 changes: 64 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/Graph/OwnedEntityGraphNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Data.Entity;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal.Graph
{
internal class OwnedEntityGraphNode : GraphNode
{
internal OwnedEntityGraphNode(GraphNode parent, PropertyInfo accessor)
: base(parent, accessor)
{
}

public override void Update<T>(DbContext context, T persisted, T updating)
{
var dbValue = GetValue<object>(persisted);
var newValue = GetValue<object>(updating);

if (dbValue == null && newValue == null)
return;

// Merging options
// 1. No new value, set value to null. entity will be removed if cascade rules set.
// 2. If new value is same as old value lets update the members
// 3. Otherwise new value is set and we don't care about old dbValue, so create a new one.
if (newValue == null)
{
SetValue(persisted, null);
return;
}

if (dbValue != null && IsKeyIdentical(context, newValue, dbValue))
UpdateValuesWithConcurrencyCheck(context, newValue, dbValue);
else
dbValue = CreateNewPersistedEntity(context, persisted, newValue);

AttachCyclicNavigationProperty(context, persisted, newValue);

// if (dbValue != null) // make an error throw or not?
foreach (var childMember in Members)
childMember.Update(context, dbValue, newValue);
}

private object CreateNewPersistedEntity<T>(DbContext context, T existing, object newValue) where T : class, new()
{
// TBD_79ad60a7-8091-4d0c-b5de-7373f3b8cedf: Could be accepted with an option. Otherwise, we cannot allow people by default to provide multiple entity with same ID if it's not the same.
//var local = context.Set(Accessor.PropertyType).Local;
//foreach (var entity in local)
//{
// if (entity.Equals(newValue))
// {
// SetValue(existing, entity);
// return entity;
// }
//}

var instance = Activator.CreateInstance(newValue.GetType());
SetValue(existing, instance);
context.Set(Accessor.PropertyType).Add(instance);
UpdateValuesWithConcurrencyCheck(context, newValue, instance);
return instance;
}
}
}
103 changes: 103 additions & 0 deletions GraphDiff/GraphDiff.Shared/Internal/GraphDiffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using RefactorThis.GraphDiff.Internal.Graph;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace RefactorThis.GraphDiff.Internal
{
/// <summary>
/// GraphDiff main access point.
/// </summary>
/// <typeparam name="T">The root agreggate type</typeparam>
internal class GraphDiffer<T> where T : class, new()
{
private readonly GraphNode _root;

public GraphDiffer(GraphNode root)
{
_root = root;
}

public T Merge(DbContext context, T updating)
{
bool isAutoDetectEnabled = context.Configuration.AutoDetectChangesEnabled;
try
{
// performance improvement for large graphs
context.Configuration.AutoDetectChangesEnabled = false;

// Get our entity with all includes needed, or add
T persisted = GetOrAddPersistedEntity(context, updating);

if (context.Entry(updating).State != EntityState.Detached)
{
throw new InvalidOperationException("GraphDiff supports detached entities only at this time. Please try AsNoTracking() or detach your entites before calling the UpdateGraph method");
}

// Perform recursive update
_root.Update(context, persisted, updating);

return persisted;
}
finally
{
context.Configuration.AutoDetectChangesEnabled = isAutoDetectEnabled;
}
}

private T GetOrAddPersistedEntity(DbContext context, T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

var persisted = FindEntityMatching(context, entity);

if (persisted == null)
{
// we are always working with 2 graphs, simply add a 'persisted' one if none exists,
// this ensures that only the changes we make within the bounds of the mapping are attempted.
persisted = new T();
context.Set<T>().Add(persisted);
}

return persisted;
}

private T FindEntityMatching(DbContext context, T entity)
{
var includeStrings = new List<string>();
_root.GetIncludeStrings(context, includeStrings);

// attach includes to IQueryable
var query = context.Set<T>().AsQueryable();
query = includeStrings.Aggregate(query, (current, include) => current.Include(include));

// Run the find operation
return query.SingleOrDefault(CreateKeyPredicateExpression(context, entity));
}

private static Expression<Func<T, bool>> CreateKeyPredicateExpression(IObjectContextAdapter context, T entity)
{
// get key properties of T
var keyProperties = context.GetPrimaryKeyFieldsFor(typeof(T)).ToList();

ParameterExpression parameter = Expression.Parameter(typeof(T));
Expression expression = CreateEqualsExpression(entity, keyProperties[0], parameter);
for (int i = 1; i < keyProperties.Count; i++)
expression = Expression.And(expression, CreateEqualsExpression(entity, keyProperties[i], parameter));

return Expression.Lambda<Func<T, bool>>(expression, parameter);
}

private static Expression CreateEqualsExpression(object entity, PropertyInfo keyProperty, Expression parameter)
{
return Expression.Equal(Expression.Property(parameter, keyProperty), Expression.Constant(keyProperty.GetValue(entity, null), keyProperty.PropertyType));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="EntityFramework" Version="6.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="1.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\GraphDiff.NetStandard21\GraphDiff.NetStandard21.csproj" />
</ItemGroup>

<Import Project="..\GraphDiff.Tests.Shared\GraphDiff.Tests.Shared.projitems" Label="Shared" />

</Project>
23 changes: 23 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Bootstrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Data.Entity;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RefactorThis.GraphDiff.Tests
{
[TestClass]
public class Bootstrapper
{
[AssemblyInitialize]
public static void Initialize(TestContext context)
{
Database.SetInitializer(new DropCreateDatabaseAlways<TestDbContext>());
using (var db = new TestDbContext())
{
if (db.Database.Exists())
{
db.Database.Delete();
}
db.Database.Create();
}
}
}
}
30 changes: 30 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/GraphDiff.Tests.Shared.projitems
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>f49226dd-5bf7-482d-b2a3-88cbe41d018b</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>GraphDiff.Tests.Shared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Bootstrapper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\TestModels.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TestBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TestDbContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\AddAggregateBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\AssociatedCollectionBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\AssociatedEntityBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\AttachedBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\ConfigurationVisitorBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\ErrorHandlingBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\GuidKeyBehaviors.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\MiscBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\OneMemberBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\OptimisticConcurrencyBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\OwnedCollectionBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\OwnedEntityBehaviours.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tests\ThirdTierBehaviours.cs" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/GraphDiff.Tests.Shared.shproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>f49226dd-5bf7-482d-b2a3-88cbe41d018b</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="GraphDiff.Tests.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
298 changes: 298 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Models/TestModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RefactorThis.GraphDiff.Tests.Models
{

// ====================================
// First tier models
// ====================================

public class Entity
{
[Key]
public int Id { get; set; }

[MaxLength(128)]
public string Title { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }
}

public class TestNode : Entity
{
public OneToOneOwnedModel OneToOneOwned { get; set; }
public OneToOneAssociatedModel OneToOneAssociated { get; set; }

public ICollection<OneToManyOwnedModel> OneToManyOwned { get; set; }
public ICollection<OneToManyAssociatedModel> OneToManyAssociated { get; set; }

//public ManyToOneModel ManyToOneOwned { get; set; }
//public ManyToOneModel ManyToOneAssociated { get; set; }

//public ICollection<ManyToManyModel> ManyToManyAssociated { get; set; }
}

public class NodeGroup : Entity
{
public List<GroupedTestNode> Members { get; set; }
}

public class GroupedTestNode : TestNode
{
public GroupedTestNode One { get; set; }

public GroupedTestNode Two { get; set; }

public NodeGroup Group { get; set; }
}

public class TestChildNode : TestNode
{
}

public class TestNodeWithBaseReference : Entity
{
public TestNode OneToOneOwnedBase { get; set; }
}

public class RootEntity : Entity
{
[Required]
public RequiredAssociate RequiredAssociate { get; set; }

public List<RootEntity> Sources { get; set; }

public int? TargetId { get; set; }
public RootEntity Target { get; set; }
}

public class RequiredAssociate : Entity
{
public List<RootEntity> RootEntities { get; set; }
}

public class MultiKeyModel
{
[Key]
[Column(Order=1)]
public string KeyPart1 { get; set; }

[Key]
[Column(Order = 2)]
public string KeyPart2 { get; set; }

public string Title { get; set; }
public DateTime Date { get; set; }
}

public class InternalKeyModel
{
[Key]
internal int Id { get; set; }

internal List<InternalKeyAssociate> Associates { get; set; }
}

public class InternalKeyAssociate
{
[Key]
internal int Id { get; set; }

internal InternalKeyModel Parent { get; set; }
}

public class NullableKeyModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid? Id { get; set; }
}

public class TestNodeForIListMultiAddition : Entity
{
// If you change this to ICollection, then test TestIListOwnedCollectionAdditionDoesNotMultiAdd will pass
public IList<TestSubNodeForIListMultiAddition> SubNodes { get; set; }
}

public class TestSubNodeForIListMultiAddition
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Column(Order = 1)]
public int TestNodeForIListMultiAdditionId { get; set; }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Column(Order = 2)]
public int OtherKeyId { get; set; }

public string Title { get; set; }
}

// Generic Collection models:
public class SimpleTitleModel
{
[Key]
public string Title { get; set; }
}

public class CollectionFromListModel : List<SimpleTitleModel>
{

}

public class CollectionFromListEntity : Entity
{
public CollectionFromListModel CollectionItems { get; set; }

public List<SimpleTitleModel> SimpleTitleItems { get; set; }
}

// ====================================
// Second tier models
// ====================================
// Different classes for each as they will be mapped to DB tables with specific relations/constraints by EF
// model builder. names may seem confusing but allow us to ensure we have tests for all scenarios.

public class OneToOneAssociatedModel : Entity
{
public TestNode OneParent { get; set; }
}

public class OneToOneOwnedModel : Entity
{
public TestNode OneParent { get; set; }
public ICollection<OneToOneOneToManyOwnedModel> OneToOneOneToManyOwned { get; set; }
public OneToOneOneToOneOwnedModel OneToOneOneToOneOwned { get; set; }
public ICollection<OneToOneOneToManyAssociatedModel> OneToOneOneToManyAssociated { get; set; }
public OneToOneOneToOneAssociatedModel OneToOneOneToOneAssociated { get; set; }
}

public class OneToManyAssociatedModel : Entity
{
public TestNode OneParent { get; set; }
}

public class OneToManyOwnedModel : Entity
{
public TestNode OneParent { get; set; }
public ICollection<OneToManyOneToManyOwnedModel> OneToManyOneToManyOwned { get; set; }
public OneToManyOneToOneOwnedModel OneToManyOneToOneOwned { get; set; }
public ICollection<OneToManyOneToManyAssociatedModel> OneToManyOneToManyAssociated { get; set; }
public OneToManyOneToOneAssociatedModel OneToManyOneToOneAssociated { get; set; }
}

/*
public class ManyToOneModel : Entity
{
public ICollection<TestNode> ManyParents { get; set; }
}
public class ManyToManyModel : Entity
{
public ICollection<TestNode> ManyParents { get; set; }
}
* */

// ====================================
// Third tier models
// ====================================

public class OneToManyOneToOneOwnedModel : Entity
{
public OneToManyOwnedModel OneParent { get; set; }
}

public class OneToManyOneToManyOwnedModel : Entity
{
public OneToManyOwnedModel OneParent { get; set; }
}

public class OneToManyOneToOneAssociatedModel : Entity
{
public OneToManyOwnedModel OneParent { get; set; }
}

public class OneToManyOneToManyAssociatedModel : Entity
{
public OneToManyOwnedModel OneParent { get; set; }
}

public class OneToOneOneToOneOwnedModel : Entity
{
public OneToOneOwnedModel OneParent { get; set; }
}

public class OneToOneOneToManyOwnedModel : Entity
{
public OneToOneOwnedModel OneParent { get; set; }
}

public class OneToOneOneToOneAssociatedModel : Entity
{
public OneToOneOwnedModel OneParent { get; set; }
}

public class OneToOneOneToManyAssociatedModel : Entity
{
public OneToOneOwnedModel OneParent { get; set; }
}

public class ModelRoot
{
public Guid Id { get; set; }
public virtual ICollection<ModelLevel1> MyModelsLevel1 { get; set; }
}

public class ModelLevel1
{
protected bool Equals(ModelLevel1 other)
{
return Id.Equals(other.Id);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ModelLevel1)obj);
}

public override int GetHashCode()
{
return Id.GetHashCode();
}

public Guid Id { get; set; }

public virtual ModelLevel2 ModelLevel2 { get; set; }
}

public class ModelLevel2
{
protected bool Equals(ModelLevel2 other)
{
return Code.Equals(other.Code);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ModelLevel2)obj);
}

public override int GetHashCode()
{
return Code.GetHashCode();
}

public Guid Code { get; set; }
public string Name { get; set; }
}
}
24 changes: 24 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/TestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Transactions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RefactorThis.GraphDiff.Tests
{
public class TestBase
{
private TransactionScope _transactionScope;

[TestInitialize]
public void CreateTransactionOnTestInitialize()
{
_transactionScope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { Timeout = new TimeSpan(0, 10, 0) });
}

[TestCleanup]
public void DisposeTransactionOnTestCleanup()
{
Transaction.Current.Rollback();
_transactionScope.Dispose();
}
}
}
80 changes: 80 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/TestDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Data.Entity;
using RefactorThis.GraphDiff.Tests.Models;
using RefactorThis.GraphDiff.Tests.Tests;

namespace RefactorThis.GraphDiff.Tests
{
public class TestDbContext : DbContext
{
public IDbSet<CollectionFromListEntity> CollectionFromListEntities { get; set; }
public IDbSet<TestNode> Nodes { get; set; }
public IDbSet<TestNodeWithBaseReference> NodesWithReference { get; set; }

public IDbSet<NodeGroup> NodeGroups { get; set; }

public IDbSet<OneToOneOwnedModel> OneToOneOwnedModels { get; set; }
public IDbSet<OneToOneAssociatedModel> OneToOneAssociatedModels { get; set; }
public IDbSet<OneToManyAssociatedModel> OneToManyAssociatedModels { get; set; }
public IDbSet<OneToManyOwnedModel> OneToManyOwnedModels { get; set; }



public IDbSet<MultiKeyModel> MultiKeyModels { get; set; }

public IDbSet<RootEntity> RootEntities { get; set; }

public IDbSet<RequiredAssociate> RequiredAssociates { get; set; }
public IDbSet<GuidEntity> GuidKeyModels { get; set; }
public IDbSet<InternalKeyModel> InternalKeyModels { get; set; }
public IDbSet<NullableKeyModel> NullableKeyModels { get; set; }

public IDbSet<TestNodeForIListMultiAddition> TestNodesForIListMultiAddition { get; set; }
public IDbSet<TestSubNodeForIListMultiAddition> TestSubNodesForIListMultiAddition { get; set; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// second tier mappings

modelBuilder.Entity<TestNodeForIListMultiAddition>().HasMany(p => p.SubNodes).WithRequired().HasForeignKey(s => s.TestNodeForIListMultiAdditionId).WillCascadeOnDelete(true);
modelBuilder.Entity<TestNode>().HasOptional(p => p.OneToOneAssociated).WithOptionalPrincipal(p => p.OneParent);
modelBuilder.Entity<TestNode>().HasMany(p => p.OneToManyAssociated).WithOptional(p => p.OneParent);
modelBuilder.Entity<TestNode>().HasOptional(p => p.OneToOneOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();
modelBuilder.Entity<TestNode>().HasMany(p => p.OneToManyOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();

modelBuilder.Entity<GroupedTestNode>().HasOptional(g => g.One).WithOptionalDependent(g => g.Two).WillCascadeOnDelete(false);

// third tier mappings

modelBuilder.Entity<OneToManyOwnedModel>().HasOptional(p => p.OneToManyOneToOneAssociated).WithOptionalPrincipal(p => p.OneParent);
modelBuilder.Entity<OneToManyOwnedModel>().HasMany(p => p.OneToManyOneToManyAssociated).WithOptional(p => p.OneParent);
modelBuilder.Entity<OneToManyOwnedModel>().HasOptional(p => p.OneToManyOneToOneOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();
modelBuilder.Entity<OneToManyOwnedModel>().HasMany(p => p.OneToManyOneToManyOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();

modelBuilder.Entity<OneToOneOwnedModel>().HasOptional(p => p.OneToOneOneToOneAssociated).WithOptionalPrincipal(p => p.OneParent);
modelBuilder.Entity<OneToOneOwnedModel>().HasMany(p => p.OneToOneOneToManyAssociated).WithOptional(p => p.OneParent);
modelBuilder.Entity<OneToOneOwnedModel>().HasOptional(p => p.OneToOneOneToOneOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();
modelBuilder.Entity<OneToOneOwnedModel>().HasMany(p => p.OneToOneOneToManyOwned).WithRequired(p => p.OneParent).WillCascadeOnDelete();

modelBuilder.Entity<RootEntity>().HasOptional(c => c.Target).WithMany(c => c.Sources);

// Guid mappings

modelBuilder.Entity<GuidTestNode>().HasOptional(p => p.OneToOneOwned).WithRequired(p => p.OneParent);

// Internal mappings

modelBuilder.Entity<InternalKeyModel>().HasKey(i => i.Id);
modelBuilder.Entity<InternalKeyAssociate>().HasKey(i => i.Id);

modelBuilder.Entity<InternalKeyModel>()
.HasMany(ikm => ikm.Associates)
.WithRequired(ikm => ikm.Parent);

modelBuilder.Entity<ModelRoot>().HasKey(x => x.Id).HasMany(x => x.MyModelsLevel1);
modelBuilder.Entity<ModelLevel1>().HasKey(x => x.Id).HasOptional(x => x.ModelLevel2);
modelBuilder.Entity<ModelLevel2>().HasKey(x => x.Code);
}

public TestDbContext() : base("Server=localhost;Initial Catalog=GraphDiff;Integrated Security=true;Connection Timeout = 300;Persist Security Info=True;") {}
}
}
200 changes: 200 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/AddAggregateBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using System;
using System.Data.Entity;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;
using System.Linq;
using System.Collections.Generic;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class AddAggregateBehaviours : TestBase
{
[TestMethod]
public void ShouldAddNewAggregateRootWithDuplicateEntityOnLevel1()
{
var guid = Guid.Parse("C1775DA8-61EC-47A1-ADF9-A50653820061");

var node1 = new ModelRoot()
{
Id = Guid.NewGuid(),
MyModelsLevel1 = new List<ModelLevel1>()
{
new ModelLevel1() {Id = guid},
new ModelLevel1() {Id = guid}
}
};

using (var context = new TestDbContext())
{
node1 = context.UpdateGraph(node1, map =>
map.OwnedCollection(p => p.MyModelsLevel1,
with => with.OwnedEntity(p => p.ModelLevel2)));

context.SaveChanges();
}

using (var context = new TestDbContext())
{
var model = context.Set<ModelRoot>().Include(x => x.MyModelsLevel1).FirstOrDefault();
Assert.IsTrue(model.MyModelsLevel1.All(x => x.Id == guid));
}
}

[TestMethod]
public void ShouldAddNewAggregateRootWithduplicateEntityOnLevel2()
{
// See TBD_79ad60a7-8091-4d0c-b5de-7373f3b8cedf
//var guid = Guid.Parse("C1775DA8-61EC-47A1-ADF9-A50653820061");

//var node1 = new ModelRoot()
//{
// Id = Guid.NewGuid(),
// MyModelsLevel1 = new List<ModelLevel1>()
// {
// new ModelLevel1() {Id = Guid.NewGuid(), ModelLevel2 = new ModelLevel2() {Code = guid}},
// new ModelLevel1() {Id = Guid.NewGuid(), ModelLevel2 = new ModelLevel2() {Code = guid}}
// }
//};

//using (var context = new TestDbContext())
//{
// node1 = context.UpdateGraph(node1, map =>
// map.OwnedCollection(p => p.MyModelsLevel1,
// with => with.OwnedEntity(p => p.ModelLevel2)));

// context.SaveChanges();
//}

//using (var context = new TestDbContext())
//{
// var models = context.Set<ModelLevel1>().Include(x => x.ModelLevel2).ToList();
// Assert.IsTrue(models.All(x => x.ModelLevel2.Code == guid));
//}
}

[TestMethod]
public void ShouldAddNewAggregateRoot_Detached()
{
var associated = new OneToOneAssociatedModel { Title = "Associated" };
var manyAssociated = new OneToManyAssociatedModel { Title = "Associated" };
var node1 = new TestNode
{
Title = "New Node",
OneToManyOwned = new List<OneToManyOwnedModel>
{
new OneToManyOwnedModel { Title = "One" },
new OneToManyOwnedModel { Title = "Two" },
new OneToManyOwnedModel { Title = "Three" }
},
OneToManyAssociated = new List<OneToManyAssociatedModel>
{
manyAssociated
},
OneToOneOwned = new OneToOneOwnedModel { Title = "OneToOne" },
OneToOneAssociated = associated
};

using (var context = new TestDbContext())
{
context.OneToManyAssociatedModels.Add(manyAssociated);
context.OneToOneAssociatedModels.Add(associated);
context.SaveChanges();
} // Simulate detach

using (var context = new TestDbContext())
{
// Setup mapping
node1 = context.UpdateGraph(node1, map => map
.OwnedEntity(p => p.OneToOneOwned)
.AssociatedEntity(p => p.OneToOneAssociated)
.OwnedCollection(p => p.OneToManyOwned, with => with.OwnedEntity(p => p.OneToManyOneToOneOwned))
.AssociatedCollection(p => p.OneToManyAssociated));

context.SaveChanges();
Assert.IsNotNull(context.Nodes.SingleOrDefault(p => p.Id == node1.Id));
}
}

[TestMethod]
public void ShouldAddNewAggregateRoot_Attached()
{
var associated = new OneToOneAssociatedModel { Title = "Associated" };
var manyAssociated = new OneToManyAssociatedModel { Title = "Associated" };
var node1 = new TestNode
{
Title = "New Node",
OneToManyOwned = new List<OneToManyOwnedModel>
{
new OneToManyOwnedModel { Title = "One" },
new OneToManyOwnedModel { Title = "Two" },
new OneToManyOwnedModel { Title = "Three" }
},
OneToManyAssociated = new List<OneToManyAssociatedModel>
{
manyAssociated
},
OneToOneOwned = new OneToOneOwnedModel { Title = "OneToOne" },
OneToOneAssociated = associated
};

using (var context = new TestDbContext())
{
context.OneToManyAssociatedModels.Add(manyAssociated);
context.OneToOneAssociatedModels.Add(associated);

// Setup mapping
node1 = context.UpdateGraph(node1, map => map
.OwnedEntity(p => p.OneToOneOwned)
.AssociatedEntity(p => p.OneToOneAssociated)
.OwnedCollection(p => p.OneToManyOwned)
.AssociatedCollection(p => p.OneToManyAssociated));

context.SaveChanges();
Assert.IsNotNull(context.Nodes.SingleOrDefault(p => p.Id == node1.Id));
}
}

[TestMethod]
public void ShouldAddNewAggregateWithOwnedEntityAndOwnedCollection()
{
var node1 = new TestNode
{
Title = "New Node",
OneToOneOwned = new OneToOneOwnedModel
{
OneToOneOneToManyOwned = new[]
{
new OneToOneOneToManyOwnedModel {Title = "One"},
new OneToOneOneToManyOwnedModel {Title = "Two"},
new OneToOneOneToManyOwnedModel {Title = "Three"}
}
}
};

using (var context = new TestDbContext())
{
node1 = context.UpdateGraph(node1, map => map.OwnedEntity(p => p.OneToOneOwned, with =>
with.OwnedCollection(p => p.OneToOneOneToManyOwned)));
context.SaveChanges();
}

using (var context = new TestDbContext())
{
var reload = context.Nodes
.Include("OneToOneOwned.OneToOneOneToManyOwned")
.SingleOrDefault(p => p.Id == node1.Id);

Assert.IsNotNull(reload);
Assert.AreEqual(node1.Title, reload.Title);
Assert.IsNotNull(reload.OneToOneOwned);
Assert.AreEqual(node1.OneToOneOwned.Id, reload.OneToOneOwned.Id);

Assert.IsNotNull(reload.OneToOneOwned.OneToOneOneToManyOwned);
Assert.AreEqual(3, reload.OneToOneOwned.OneToOneOneToManyOwned.Count);
Assert.AreEqual(node1.OneToOneOwned.OneToOneOneToManyOwned.First().Id, node1.OneToOneOwned.OneToOneOneToManyOwned.First().Id);

}
}
}
}

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/AssociatedEntityBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Linq;
using System.Data.Entity;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class AssociatedEntityBehaviours : TestBase
{
[TestMethod]
public void ShouldAddRelationIfPreviousValueWasNull()
{
var node1 = new TestNode { Title = "New Node" };
var associated = new OneToOneAssociatedModel { Title = "Associated Node" };

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.OneToOneAssociatedModels.Add(associated);
context.SaveChanges();
} // Simulate detach

node1.OneToOneAssociated = associated;

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, map => map
.AssociatedEntity(p => p.OneToOneAssociated));

context.SaveChanges();
var node2 = context.Nodes.Include(p => p.OneToOneAssociated).Single(p => p.Id == node1.Id);
Assert.IsNotNull(node2);
Assert.IsTrue(node2.OneToOneAssociated.OneParent == node2);
}
}

[TestMethod]
public void ShouldAddRelationIfPreviousValueWasNullWithCycle()
{
GroupedTestNode two;
GroupedTestNode one;
using (var context = new TestDbContext())
{
var group = new NodeGroup();
context.NodeGroups.Add(group);
context.SaveChanges();

one = new GroupedTestNode { Group = group };
context.Nodes.Add(one);
context.SaveChanges();

two = new GroupedTestNode { Group = group };
context.Nodes.Add(two);
context.SaveChanges();

Assert.AreEqual(2, group.Members.Count);
} // Simulate detach

using (var context = new TestDbContext())
{
one.Two = two;

// Setup mapping
context.UpdateGraph(one, map => map.AssociatedEntity(o => o.Two));
context.SaveChanges();

var oneReloaded = context.Nodes.OfType<GroupedTestNode>().Include("Two").Single(n => n.Id == one.Id);
Assert.IsNotNull(oneReloaded.Two);
Assert.AreEqual(two.Id, oneReloaded.Two.Id);
}
}

[TestMethod]
public void ShouldRemoveAssociatedRelationIfNull()
{
var node1 = new TestNode { Title = "New Node", OneToOneAssociated = new OneToOneAssociatedModel { Title = "Associated Node" } };

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach

node1.OneToOneAssociated = null;

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, map => map
.AssociatedEntity(p => p.OneToOneAssociated));

context.SaveChanges();
var node2 = context.Nodes.Include(p => p.OneToOneAssociated).Single(p => p.Id == node1.Id);
Assert.IsNotNull(node2);
Assert.IsTrue(node2.OneToOneAssociated == null);
}
}

[TestMethod]
public void ShouldReplaceReferenceIfNewEntityIsNotPreviousEntity()
{
var node1 = new TestNode {
Title = "New Node",
OneToOneAssociated = new OneToOneAssociatedModel { Title = "Associated Node" }
};
var otherModel = new OneToOneAssociatedModel { Title = "Hello" };

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.OneToOneAssociatedModels.Add(otherModel);
context.SaveChanges();
} // Simulate detach

node1.OneToOneAssociated = otherModel;

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, map => map
.AssociatedEntity(p => p.OneToOneAssociated));

context.SaveChanges();
var node2 = context.Nodes.Include(p => p.OneToOneAssociated).Single(p => p.Id == node1.Id);
Assert.IsNotNull(node2);
Assert.IsTrue(node2.OneToOneAssociated.OneParent == node2);
// should not delete it as it is associated and no cascade rule set.
Assert.IsTrue(context.OneToOneAssociatedModels.Single(p => p.Id != otherModel.Id).OneParent == null);
}
}

[TestMethod]
public void ShouldNotUpdatePropertiesOfAnAssociatedEntity()
{
var node1 = new TestNode
{
Title = "New Node",
OneToOneAssociated = new OneToOneAssociatedModel { Title = "Associated Node" }
};

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach


node1.OneToOneAssociated.Title = "Updated Content";

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, map => map
.AssociatedEntity(p => p.OneToOneAssociated));

context.SaveChanges();
var node2 = context.Nodes.Include(p => p.OneToOneAssociated).Single(p => p.Id == node1.Id);
Assert.IsNotNull(node2);
Assert.IsTrue(node2.OneToOneAssociated.OneParent == node2);
// should not delete it as it is associated and no cascade rule set.
Assert.IsTrue(node2.OneToOneAssociated.Title == "Associated Node");
}
}
}
}
24 changes: 24 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/AttachedBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class AttachedBehaviours
{
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ShouldThrowExceptionIfAggregateIsNotDetached()
{
using (var context = new TestDbContext())
{
var node = new TestNode();
context.Nodes.Add(node);
node.Title = "Hello";
context.UpdateGraph(node);
context.SaveChanges();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq.Expressions;
using RefactorThis.GraphDiff.Tests.Models;
using System.Data.Entity;
using System.Linq;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class ConfigurationVisitorBehaviours : TestBase
{
public Expression<Func<TestNode, OneToOneOwnedModel>> Lambda { get; set; }

[TestMethod]
public void ShouldBeAbleToVisitExpressionsStoredAsFields()
{
var node1 = new TestNode
{
Title = "One",
OneToOneOwned = new OneToOneOwnedModel { Title = "Hello" }
};

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach

node1.OneToOneOwned.Title = "Hey2";

Expression<Func<TestNode, OneToOneOwnedModel>> lambda = (p => p.OneToOneOwned);
Expression<Func<IUpdateConfiguration<TestNode>, dynamic>> exp = map => map.OwnedEntity(lambda);

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, exp);

context.SaveChanges();
Assert.IsTrue(context.Nodes
.Include(p => p.OneToOneOwned)
.Single(p => p.Id == node1.Id)
.OneToOneOwned.Title == "Hey2");
}
}

[TestMethod]
public void ShouldBeAbleToVisitExpressionsStoredAsProperties()
{
var node1 = new TestNode
{
Title = "One",
OneToOneOwned = new OneToOneOwnedModel { Title = "Hello" }
};

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach

node1.OneToOneOwned.Title = "Hey2";

Lambda = (p => p.OneToOneOwned);
Expression<Func<IUpdateConfiguration<TestNode>, dynamic>> exp = map => map.OwnedEntity(Lambda);

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, exp);

context.SaveChanges();
Assert.IsTrue(context.Nodes
.Include(p => p.OneToOneOwned)
.Single(p => p.Id == node1.Id)
.OneToOneOwned.Title == "Hey2");
}
}
}
}
22 changes: 22 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/ErrorHandlingBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class ErrorHandlingBehaviours : TestBase
{
internal class UnknownType { }

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ShouldThrowIfTypeIsNotKnown()
{
using (var context = new TestDbContext())
{
context.UpdateGraph(new UnknownType());
context.SaveChanges();
}
}
}
}
74 changes: 74 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/GuidKeyBehaviors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RefactorThis.GraphDiff.Tests.Tests
{
public class GuidEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
}

public class GuidTestNode : GuidEntity
{
public GuidOneToOneOwned OneToOneOwned { get; set; }
}

public class GuidOneToOneOwned : GuidEntity
{
public GuidTestNode OneParent { get; set; }
}

[TestClass]
public class GuidKeyBehaviors : TestBase
{
[TestMethod]
public void ShouldSupportGuidKeys()
{
var model = new GuidTestNode();
using (var context = new TestDbContext())
{
context.GuidKeyModels.Add(model);
context.SaveChanges();

// http://stackoverflow.com/questions/5270721/using-guid-as-pk-with-ef4-code-first
Assert.IsTrue(Attribute.IsDefined(model.GetType().GetProperty("Id"), typeof(DatabaseGeneratedAttribute)));

Assert.IsNotNull(model.Id);
Assert.AreNotEqual(Guid.Empty, model.Id);
} // simulate detach

model.OneToOneOwned = new GuidOneToOneOwned();

using (var context = new TestDbContext())
{
model = context.UpdateGraph(model, map => map.OwnedEntity(g => g.OneToOneOwned));
context.SaveChanges();

Assert.IsNotNull(model.OneToOneOwned);
Assert.IsNotNull(model.OneToOneOwned.Id);
Assert.AreNotEqual(Guid.Empty, model.OneToOneOwned.Id);
}
}

[TestMethod]
public void ShouldSupportAddingRootWithGuidKey()
{
var model = new GuidTestNode {OneToOneOwned = new GuidOneToOneOwned()};

using (var context = new TestDbContext())
{
model = context.UpdateGraph(model, map => map.OwnedEntity(g => g.OneToOneOwned));
context.SaveChanges();

Assert.AreNotEqual(Guid.Empty, model.Id);
Assert.IsNotNull(model.OneToOneOwned);
Assert.IsNotNull(model.OneToOneOwned.Id);
Assert.AreNotEqual(Guid.Empty, model.OneToOneOwned.Id);
}
}
}
}
114 changes: 114 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/MiscBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class MiscBehaviours : TestBase
{
[TestMethod]
public void ShouldSupportMultipleKeys()
{
var model = new MultiKeyModel
{
Title = "Hello",
Date = DateTime.Now,
KeyPart1 = "A123",
KeyPart2 = "A234"
};

using (var context = new TestDbContext())
{
context.MultiKeyModels.Add(model);
context.SaveChanges();
} // simulate detach

model.Date = DateTime.Parse("01/01/2010");
using (var context = new TestDbContext())
{
model = context.UpdateGraph(model);
context.SaveChanges();

context.Entry(model).Reload();
Assert.IsTrue(model.Date == DateTime.Parse("01/01/2010"));
}
}

[TestMethod]
public void ShouldSupportInternalKeys()
{
using (var context = new TestDbContext())
{
var model = context.UpdateGraph(new InternalKeyModel());
context.SaveChanges();

Assert.AreNotEqual(0, model.Id);
}
}

[TestMethod]
public void ShouldSupportInternalNavigationProperties()
{
var parent = new InternalKeyModel();
using (var context = new TestDbContext())
{
context.InternalKeyModels.Add(parent);
context.SaveChanges();
} // simulate detach

parent.Associates = new List<InternalKeyAssociate> { new InternalKeyAssociate() };

InternalKeyModel model;
using (var context = new TestDbContext())
{
model = context.UpdateGraph(parent, map => map.AssociatedCollection(ikm => ikm.Associates));
context.SaveChanges();

Assert.AreNotEqual(0, model.Id);

Assert.IsNotNull(model.Associates);
Assert.AreEqual(1, model.Associates.Count);
Assert.AreNotEqual(0, model.Associates.First().Id);
}

using (var context = new TestDbContext())
{
var reloadedModel = context.InternalKeyModels
.Include(ikm => ikm.Associates)
.SingleOrDefault(ikm => ikm.Id == model.Id);

Assert.IsNotNull(reloadedModel);
Assert.IsNotNull(reloadedModel.Associates);

Assert.AreEqual(1, reloadedModel.Associates.Count);
Assert.AreEqual(model.Associates.Single().Id, reloadedModel.Associates.Single().Id);
}
}

[TestMethod]
public void ShouldSupportNullableKeys()
{
using (var context = new TestDbContext())
context.Database.ExecuteSqlCommand("ALTER TABLE NullableKeyModels ALTER COLUMN Id uniqueidentifier NOT NULL");

NullableKeyModel model = new NullableKeyModel();
using (var context = new TestDbContext())
{
context.NullableKeyModels.Add(model);
context.SaveChanges();
}

using (var context = new TestDbContext())
{
model = context.UpdateGraph(model);
context.SaveChanges();

Assert.AreNotEqual(0, model.Id);
}
}
}
}
108 changes: 108 additions & 0 deletions GraphDiff/GraphDiff.Tests.Shared/Tests/OneMemberBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;
using System.Data.Entity;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class OneMemberGraphBehaviours : TestBase
{
[TestMethod]
public void ShouldAddSingleEntity()
{
var node1 = new TestNode
{
Title = "Hello"
};

using (var context = new TestDbContext())
{
node1 = context.UpdateGraph(node1);
context.SaveChanges();
Assert.IsNotNull(context.Nodes.SingleOrDefault(p => p.Id == node1.Id));
}
}

[TestMethod]
public void ShouldUpdateSingleEntity_Detached()
{
var node1 = new TestNode
{
Title = "Hello"
};

using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach

node1.Title = "Hello2";

using (var context = new TestDbContext())
{
context.UpdateGraph(node1);
context.SaveChanges();
Assert.IsTrue(context.Nodes.Single(p => p.Id == node1.Id).Title == "Hello2");
}
}

//[TestMethod]
//public void ShouldUpdateSingleEntity_Attached()
//{
// var node1 = new TestNode
// {
// Title = "Hello"
// };

// using (var context = new TestDbContext())
// {
// context.Nodes.Add(node1);
// node1.Title = "Hello2";
// context.UpdateGraph(node1);
// context.SaveChanges();
// Assert.IsTrue(context.Nodes.Single(p => p.Id == node1.Id).Title == "Hello2");
// }
//}

[TestMethod]
public void ShouldNotUpdateEntityIfNoChangesHaveBeenMade_Detached()
{
var node1 = new TestNode
{
Title = "Hello"
};

using (var context = new TestDbContext())
{
node1 = context.Nodes.Add(node1);
context.SaveChanges();
} // Simulate detach

using (var context = new TestDbContext())
{
context.UpdateGraph(node1);
Assert.IsTrue(context.ChangeTracker.Entries().All(p => p.State == EntityState.Unchanged));
context.SaveChanges();
}
}

//[TestMethod]
//public void ShouldNotUpdateEntityIfNoChangesHaveBeenMade_Attached()
//{
// var node1 = new TestNode
// {
// Title = "Hello"
// };

// using (var context = new TestDbContext())
// {
// node1 = context.Nodes.Add(node1);
// context.UpdateGraph(node1);
// Assert.IsTrue(context.ChangeTracker.Entries().All(p => p.State == EntityState.Added));
// context.SaveChanges();
// }
//}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Data.Entity.Infrastructure;
using System.Linq;
using RefactorThis.GraphDiff.Tests.Models;
using System.Collections.Generic;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class OptimisticConcurrencyBehaviours : TestBase
{
[TestMethod]
[ExpectedException(typeof(DbUpdateConcurrencyException))]
public void ShouldThrowDbUpdateConcurrencyExceptionIfEditingOutOfDateModel()
{
TestNode node;
using (var db = new TestDbContext())
{
node = new TestNode { Title = "Hello" };
db.Nodes.Add(node);
db.SaveChanges();
}

using (var db = new TestDbContext())
{
var node2 = new TestNode
{
RowVersion = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1 },
Id = node.Id,
Title = "Test2"
};

db.UpdateGraph(node2);
db.SaveChanges();
}
}

[TestMethod]
[ExpectedException(typeof(DbUpdateConcurrencyException))]
public void ShouldThrowDbUpdateConcurrencyExceptionIfEditingNestedOutOfDateModel()
{
TestNode node;
using (var db = new TestDbContext())
{
node = new TestNode
{
Title = "Hello",
OneToManyOwned = new List<OneToManyOwnedModel>
{
new OneToManyOwnedModel { Title = "Test1" },
new OneToManyOwnedModel { Title = "Test2" }
}
};
db.Nodes.Add(node);
db.SaveChanges();
}

using (var db = new TestDbContext())
{
node.OneToManyOwned.First().RowVersion = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1 };
db.UpdateGraph(node, map => map.OwnedCollection(p => p.OneToManyOwned));
db.SaveChanges();
}
}

[TestMethod]
[ExpectedException(typeof(DbUpdateConcurrencyException))]
public void ShouldThrowDbUpdateConcurrencyExceptionWithEmptyRowVersion()
{
TestNode node;
using (var db = new TestDbContext())
{
node = new TestNode
{
Title = "Hello",
OneToManyOwned = new List<OneToManyOwnedModel>
{
new OneToManyOwnedModel { Title = "Test1" },
new OneToManyOwnedModel { Title = "Test2" }
}
};
db.Nodes.Add(node);
db.SaveChanges();
}

using (var db = new TestDbContext())
{
node.OneToManyOwned.First().RowVersion = null;
db.UpdateGraph(node, map => map.OwnedCollection(p => p.OneToManyOwned));
db.SaveChanges();
}
}
}
}
Loading