├── .gitattributes ├── .gitignore ├── EntityGraphOperations.sln ├── EntityGraphOperations ├── Abstract │ ├── IGraphCollection.cs │ ├── IGraphNavigationalProperty.cs │ └── IManualGraphOperation.cs ├── App.config ├── Concrete │ ├── EntityConfigurations │ │ ├── ExtendedEntityTypeConfiguration.cs │ │ └── ExtendedPrimitiveTypeConfiguration.cs │ ├── GraphCollection.cs │ ├── GraphNavigationalProperty.cs │ ├── ManualGraphOperation.cs │ └── ManualOperationBuilder.cs ├── DbContextExtensions.cs ├── EntityGraphOperations.csproj ├── Internal │ ├── Compare.cs │ ├── HelperExtensions.cs │ └── Models │ │ ├── EntityRelatedProperties.cs │ │ ├── EntityRelationDirection.cs │ │ ├── EntityTree.cs │ │ ├── ExtendedPropertyInfo.cs │ │ └── ForeignKeyRelationship.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── LICENSE ├── Package.nuspec ├── README.md ├── lib └── net45 │ ├── EntityFramework.SqlServer.dll │ ├── EntityFramework.dll │ ├── EntityGraphOperations.dll │ └── Extensions.General.dll └── nuget.exe /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ -------------------------------------------------------------------------------- /EntityGraphOperations.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityGraphOperations", "EntityGraphOperations\EntityGraphOperations.csproj", "{866B4EB2-C38F-48D6-BB89-84DE03733638}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {866B4EB2-C38F-48D6-BB89-84DE03733638}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {866B4EB2-C38F-48D6-BB89-84DE03733638}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {866B4EB2-C38F-48D6-BB89-84DE03733638}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {866B4EB2-C38F-48D6-BB89-84DE03733638}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /EntityGraphOperations/Abstract/IGraphCollection.cs: -------------------------------------------------------------------------------- 1 | namespace EntityGraphOperations.Abstract 2 | { 3 | /// 4 | /// Properties and methods used by the consumer to configure the Collection properties of the entities. 5 | /// 6 | public interface IGraphCollection 7 | { 8 | IGraphCollection DeleteMissingEntities(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EntityGraphOperations/Abstract/IGraphNavigationalProperty.cs: -------------------------------------------------------------------------------- 1 | namespace EntityGraphOperations.Abstract 2 | { 3 | /// 4 | /// Properties and methods used by the consumer to configure the non-collection 5 | /// navigational properties of the entities. 6 | /// 7 | public interface IGraphNavigationProperty 8 | { 9 | IGraphNavigationProperty DeleteIfNull(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EntityGraphOperations/Abstract/IManualGraphOperation.cs: -------------------------------------------------------------------------------- 1 | using EntityGraphOperations.Concrete; 2 | using System; 3 | 4 | namespace EntityGraphOperations.Abstract 5 | { 6 | /// 7 | /// Properties and methods used by the consumer to configure state of entities manually, 8 | /// after automatically detecting all possible entities states. 9 | /// 10 | public interface IManualGraphOperation 11 | where TModel : class 12 | { 13 | IManualGraphOperation After(Action> graphBuilder); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EntityGraphOperations/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/EntityConfigurations/ExtendedEntityTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using Extensions.General; 6 | using System.Data.Entity.ModelConfiguration; 7 | 8 | namespace EntityGraphOperations.Concrete 9 | { 10 | /// 11 | /// Extended entity type configurations which allowes to automaticallt define state of entity graph. 12 | /// 13 | public class ExtendedEntityTypeConfiguration : 14 | EntityTypeConfiguration 15 | where TEntityType : class 16 | { 17 | /// 18 | /// Configures the unique key property(s) for this entity type. 19 | /// 20 | /// The type of the key. 21 | /// 22 | /// A lambda expression representing the property to be used as the unique key. 23 | /// C#: t => t.Id VB.Net: Function(t) t.Id If the primary key is made up of multiple properties then specify an anonymous type including the properties. 24 | /// C#: t => new { t.Id1, t.Id2 } 25 | /// VB.Net: Function(t) New With { t.Id1, t.Id2 } 26 | /// 27 | /// The same ModelTypeConfiguration instance so that multiple calls can be chained. 28 | public ExtendedEntityTypeConfiguration HasUniqueKey(Expression> keyExpression) 29 | { 30 | if (keyExpression == null) 31 | throw new ArgumentNullException("keyExpression"); 32 | 33 | // Get singleton instance of the configuration 34 | var modelTypeConfiguration = ExtendedEntityTypeConfiguration.GetInstance(); 35 | 36 | // If the return type is anoymous 37 | if (!keyExpression.ReturnType.IsAnonymousType()) 38 | { 39 | // Then get property info from member expression 40 | modelTypeConfiguration.UniqueKey(keyExpression.GetPropertyInfo()); 41 | } 42 | else 43 | { 44 | // Then get properties info from new expression 45 | modelTypeConfiguration.UniqueKey(keyExpression.GetPropertiesInfo()); 46 | } 47 | 48 | return this; 49 | } 50 | 51 | /// 52 | /// Returns ExtendedPrimitiveTypeConfiguration instance, which will give opportunity 53 | /// to set the property as not modifiable. 54 | /// 55 | /// Property ytpe 56 | /// 57 | /// A lambda expression representing the property to be used as the unique key. 58 | /// C#: t => t.Id VB.Net: Function(t) t.Id If the primary key is made up of multiple properties then specify an anonymous type including the properties. 59 | /// C#: t => new { t.Id1, t.Id2 } 60 | /// VB.Net: Function(t) New With { t.Id1, t.Id2 } 61 | /// 62 | /// The ExtendedPrimitiveTypeConfiguration instance so that multiple calls can be chained 63 | public ExtendedPrimitiveTypeConfiguration ExtendProperty(Expression> keyExpression) 64 | { 65 | if (keyExpression == null) 66 | throw new ArgumentNullException("keyExpression"); 67 | 68 | return new ExtendedPrimitiveTypeConfiguration(keyExpression.GetPropertyInfo()); 69 | } 70 | } 71 | 72 | /// 73 | /// Allows configuration to be performed for a model type. 74 | /// 75 | public class ExtendedEntityTypeConfiguration 76 | { 77 | #region Private fields 78 | private static readonly Lazy instance = 79 | new Lazy(() => new ExtendedEntityTypeConfiguration()); 80 | 81 | private readonly List uniqueKeyProperties = new List(); 82 | private readonly List notCompareProperties = new List(); 83 | #endregion 84 | 85 | #region Constructors 86 | // Constructor must be private for singleton pattern 87 | private ExtendedEntityTypeConfiguration() { } 88 | #endregion 89 | 90 | // Get static instance 91 | public static ExtendedEntityTypeConfiguration GetInstance() 92 | { 93 | return instance.Value; 94 | } 95 | 96 | // Add properties to the unique keys list 97 | public void UniqueKey(params PropertyInfo[] properties) 98 | { 99 | uniqueKeyProperties.AddRange(properties); 100 | } 101 | 102 | // Add properties to the list of properties which must not be modified 103 | public void NotCompare(PropertyInfo property) 104 | { 105 | notCompareProperties.Add(property); 106 | } 107 | 108 | // Get all unique properties 109 | internal IEnumerable UniqueKeys 110 | { 111 | get 112 | { 113 | return uniqueKeyProperties; 114 | } 115 | } 116 | 117 | // Get all properties which must not be modified 118 | internal IEnumerable NotCompareProperties 119 | { 120 | get 121 | { 122 | return notCompareProperties; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/EntityConfigurations/ExtendedPrimitiveTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace EntityGraphOperations.Concrete 4 | { 5 | /// 6 | /// Used to configure a primitive property of an entity. 7 | /// 8 | public class ExtendedPrimitiveTypeConfiguration 9 | { 10 | private readonly PropertyInfo propertyInfo; 11 | 12 | public ExtendedPrimitiveTypeConfiguration(PropertyInfo propertyInfo) 13 | { 14 | this.propertyInfo = propertyInfo; 15 | } 16 | 17 | /// 18 | /// This property will be set only when inserting new entity and will never be set as modified. 19 | /// 20 | /// The same ExtendedPrimitiveTypeConfiguration instance so that multiple calls can be chained 21 | public ExtendedPrimitiveTypeConfiguration NotCompare() 22 | { 23 | ExtendedEntityTypeConfiguration.GetInstance() 24 | .NotCompare(propertyInfo); 25 | 26 | return this; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/GraphCollection.cs: -------------------------------------------------------------------------------- 1 | using EntityGraphOperations.Abstract; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Extensions.General; 6 | using EntityGraphOperations.Internal; 7 | using System.Data.Entity; 8 | using System.Linq.Expressions; 9 | 10 | namespace EntityGraphOperations.Concrete 11 | { 12 | /// 13 | /// Represents a collection of entity. 14 | /// 15 | /// Type of the entity which is owner of the collection entity 16 | /// The type of the elements of the collection entity. 17 | public class GraphCollection : IGraphCollection 18 | where TEntity : class 19 | where TChildEntity : class 20 | { 21 | private DbContext context; 22 | private TEntity owner; 23 | private ICollection collection; 24 | 25 | /// 26 | /// Constructor. 27 | /// 28 | /// Lambda expression identifying the collection of the entity. 29 | /// DbContext instance 30 | /// Owner entity. 31 | public GraphCollection( 32 | Expression>> expression, 33 | DbContext context, 34 | TEntity owner) 35 | { 36 | this.context = context; 37 | this.owner = owner; 38 | this.collection = expression.Compile()(owner); 39 | } 40 | 41 | /// 42 | /// Retrieve list of the owner's previous collection of TChildEntity type. 43 | /// Then, compares it with the current collection. Missing entities state will be set to Deleted. 44 | /// 45 | /// An IGraphCollection instance. 46 | public IGraphCollection DeleteMissingEntities() 47 | { 48 | var relationship = context.GetForeignKeyRelationship(); 49 | 50 | // Foreign key Properties in collection entities and their values from the owner entity 51 | var properties = relationship 52 | .DependantProperties 53 | .Select((x, index) => new ExtendedPropertyInfo() 54 | { 55 | PropertyInfo = x, 56 | PropertyValue = owner.GetValue(relationship.PrincipalProperties.ElementAt(index)) 57 | }); 58 | 59 | // Get previous collection elements from the database 60 | var missedEntities = context.Set() 61 | .Where(properties) 62 | .AsNoTracking() 63 | .ToList(); 64 | 65 | // If current collection is null or empty, then remove all elements from the table 66 | // which are belongs to the owner. 67 | if (collection == null || !collection.Any()) 68 | missedEntities 69 | .ForEach(x => context.Entry(x).State = EntityState.Deleted); 70 | else 71 | { 72 | // Get Primary key of child entities 73 | var primaryKeyOfCollectionElements = context.GetPrimaryKeys(); 74 | 75 | // And internally search for all of them in current collection. 76 | // Set their state to deleted, if they are missing from the current collection. 77 | missedEntities.ForEach(item => 78 | { 79 | var exactElementPk = primaryKeyOfCollectionElements 80 | .Select(x => new ExtendedPropertyInfo() 81 | { 82 | PropertyInfo = x, 83 | PropertyValue = item.GetValue(x) 84 | }); 85 | 86 | if (!collection.AsQueryable().Where(exactElementPk).Any()) 87 | context.Entry(item).State = EntityState.Deleted; 88 | }); 89 | } 90 | 91 | return this; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/GraphNavigationalProperty.cs: -------------------------------------------------------------------------------- 1 | using EntityGraphOperations.Abstract; 2 | using System; 3 | using System.Linq; 4 | using Extensions.General; 5 | using EntityGraphOperations.Internal; 6 | using System.Data.Entity; 7 | using System.Linq.Expressions; 8 | 9 | namespace EntityGraphOperations.Concrete 10 | { 11 | /// 12 | /// Represents a navigational property in an entity. 13 | /// 14 | /// Type of the entity which is owner of the navigational property. 15 | /// The type of the navigational property. 16 | public class GraphNavigationalProperty : IGraphNavigationProperty 17 | where TEntity : class 18 | where TChildEntity : class 19 | { 20 | private DbContext context; 21 | private TEntity owner; 22 | private TChildEntity navigationalProperty; 23 | 24 | /// 25 | /// Constructor. 26 | /// 27 | /// Lambda expression identifying the navigational property of the entity. 28 | /// DbContext instance 29 | /// Owner entity. 30 | public GraphNavigationalProperty( 31 | Expression> expression, 32 | DbContext context, 33 | TEntity owner) 34 | { 35 | this.context = context; 36 | this.owner = owner; 37 | this.navigationalProperty = expression.Compile()(owner); 38 | } 39 | 40 | /// 41 | /// Delete entity if was existed for the owner entity and has null value now. 42 | /// 43 | /// An IGraphNavigationProperty instance 44 | public IGraphNavigationProperty DeleteIfNull() 45 | { 46 | var relationship = context.GetForeignKeyRelationship(); 47 | 48 | // Foreign key Properties in navigational property and it's value from the owner entity 49 | var properties = relationship 50 | .DependantProperties 51 | .Select((x, index) => new ExtendedPropertyInfo() 52 | { 53 | PropertyInfo = x, 54 | PropertyValue = owner.GetValue(relationship.PrincipalProperties.ElementAt(index)) 55 | }); 56 | 57 | // Get previous element from the database 58 | var previousEntity = context.Set() 59 | .Where(properties) 60 | .AsNoTracking() 61 | .SingleOrDefault(); 62 | 63 | // If current collection is null or empty, then remove all elements from the table 64 | // which are belongs to the owner. 65 | if (navigationalProperty == null && 66 | previousEntity != null) 67 | context.Entry(previousEntity).State = EntityState.Deleted; 68 | 69 | return this; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/ManualGraphOperation.cs: -------------------------------------------------------------------------------- 1 | using EntityGraphOperations.Abstract; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data.Entity; 5 | using System.Linq.Expressions; 6 | 7 | namespace EntityGraphOperations.Concrete 8 | { 9 | /// 10 | /// Manually manage entities states after automatically detecting all possible entities states. 11 | /// 12 | /// Type of the entity which is owner of the collection entity. 13 | public class ManualGraphOperation 14 | : IManualGraphOperation 15 | where TEntity : class 16 | { 17 | private DbContext context { get; set; } 18 | private TEntity model { get; set; } 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | /// DbContext instance. 24 | /// Type of the owner entity. 25 | public ManualGraphOperation(DbContext context, TEntity model) 26 | { 27 | this.context = context; 28 | this.model = model; 29 | } 30 | 31 | /// 32 | /// Creates an instance of the IManualGraphOperation to configure operation. 33 | /// 34 | /// Delegate to create an instance of ManualOperationBuilder. 35 | /// An instance of IManualGraphOperation. 36 | public IManualGraphOperation After(Action> manualOperationBuilderAction) 37 | { 38 | ManualOperationBuilder manualOperationBuilder = new ManualOperationBuilder(this); 39 | manualOperationBuilderAction(manualOperationBuilder); 40 | return this; 41 | } 42 | 43 | /// 44 | /// Registers an lambda expression as collection member and 45 | /// returns IGraphCollection for additional configurations over this collection. 46 | /// 47 | /// The type of the elements of the collection. 48 | /// Lambda expression identifying the collection of the entity. 49 | /// An instance of the IGraphCollection 50 | internal IGraphCollection RegisterCollection(Expression>> expression) 51 | where TChildEntity : class 52 | { 53 | return new GraphCollection(expression, context, model); 54 | } 55 | 56 | /// 57 | /// Registers an lambda expression as navigational property member and 58 | /// returns IGraphNavigationProperty for additional configurations over this property. 59 | /// 60 | /// The type of the navigational property. 61 | /// Lambda expression identifying the navigational property of the model. 62 | /// An instance of the IGraphNavigationProperty 63 | internal IGraphNavigationProperty RegisterNavigationalProperty(Expression> expression) 64 | where TChildEntity : class 65 | { 66 | return new GraphNavigationalProperty(expression, context, model); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /EntityGraphOperations/Concrete/ManualOperationBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using EntityGraphOperations.Abstract; 5 | 6 | namespace EntityGraphOperations.Concrete 7 | { 8 | /// 9 | /// Creates instances of the GraphCollections. 10 | /// 11 | /// Type of the entity which is owner of the collection entity. 12 | public class ManualOperationBuilder 13 | where TEntity : class 14 | { 15 | private ManualGraphOperation graphOperation; 16 | 17 | /// 18 | /// Constrcutor. 19 | /// 20 | /// Instance of the ManualGraphOperation. 21 | public ManualOperationBuilder(ManualGraphOperation graphOperation) 22 | { 23 | this.graphOperation = graphOperation; 24 | } 25 | 26 | /// 27 | /// Creates instance of the IGraphCollection for this collection of the current owner entity. 28 | /// 29 | /// The type of the elements of the collection. 30 | /// Lambda expression identifying the collection of the model 31 | /// An instance of the IGraphCollection 32 | public IGraphCollection HasCollection(Expression>> expression) 33 | where TChildEntity : class 34 | { 35 | return graphOperation.RegisterCollection(expression); 36 | } 37 | 38 | /// 39 | /// Creates instance of the IGraphNavigationProperty for this property of the current owner entity. 40 | /// 41 | /// The type of the navigational property. 42 | /// Lambda expression identifying the navigational property of the model. 43 | /// An instance of the IGraphNavigationProperty 44 | public IGraphNavigationProperty HasNavigationalProperty(Expression> expression) 45 | where TChildEntity : class 46 | { 47 | return graphOperation.RegisterNavigationalProperty(expression); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /EntityGraphOperations/DbContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using EntityGraphOperations.Concrete; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data.Entity; 5 | using System.Linq; 6 | using Extensions.General; 7 | using System.Data.Entity.Infrastructure; 8 | using EntityGraphOperations.Internal; 9 | 10 | namespace EntityGraphOperations 11 | { 12 | public static class DbContextExtensions 13 | { 14 | /// 15 | /// Find mathcing entity in context based on primary or unique properties of the given entity instance 16 | /// 17 | /// Entity type 18 | /// Context instance 19 | /// Entity instance 20 | /// If true, then query will send to the database; otherwise to .net object 21 | /// Returns queryable after filtering the source 22 | public static IQueryable FindMatchingEntity( 23 | this DbContext context, 24 | TSource entity, 25 | bool isLocal = false) 26 | where TSource : class 27 | { 28 | if (context == null) 29 | throw new ArgumentNullException("context"); 30 | 31 | if (entity == null) 32 | throw new ArgumentNullException("entity"); 33 | 34 | var source = isLocal ? 35 | // Search local for duplicate entities 36 | context.Set().Local.AsQueryable() : 37 | // Send query to the database 38 | context.Set(); 39 | 40 | var properties = context.GetPropertiesForFiltering(entity, new List()); 41 | 42 | if (properties == null || !properties.Any()) 43 | return null; 44 | 45 | return source.Where(properties); 46 | } 47 | 48 | /// 49 | /// Find all entities which their state must be defined after searching for them. 50 | /// Get these entities alongside with their principle and dependant entities. 51 | /// Then define state for all of them in ascending order by their direct and indirect principles. 52 | /// 53 | /// Entity type 54 | /// DbContext instance 55 | /// Entity instance 56 | public static ManualGraphOperation InsertOrUpdateGraph( 57 | this DbContext context, 58 | TEntity entity) 59 | where TEntity : class 60 | { 61 | if (entity == null) 62 | throw new ArgumentNullException("entity"); 63 | 64 | if (context == null) 65 | throw new ArgumentNullException("context"); 66 | 67 | context.Entry(entity).State = EntityState.Added; 68 | 69 | var mainEntityTree = new EntityTree { Entity = entity }; 70 | var wholeEntityTree = new List() { mainEntityTree }; 71 | 72 | // Set all entities with their dependant and principle properties 73 | context.GetEntityTree(entity, null, null, wholeEntityTree); 74 | 75 | // Get all entities direct and indirect principles count 76 | var entitiesInDefineStateOrder = wholeEntityTree 77 | .Select(curr => new 78 | { 79 | currentTree = curr, 80 | IndirectPrincipleCount = curr.GetIndirectPrinciples(wholeEntityTree).Count() 81 | }) 82 | .OrderBy(x => x.IndirectPrincipleCount); 83 | 84 | foreach (var item in entitiesInDefineStateOrder) 85 | { 86 | // Entity state will be detached if there is some duplicate entities 87 | if (context.Entry(item.currentTree.Entity).State == EntityState.Detached) 88 | continue; 89 | 90 | // Define state of entity if it is not in detached state 91 | DefineState( 92 | context, 93 | item.currentTree.Entity as dynamic, 94 | wholeEntityTree); 95 | } 96 | 97 | return new ManualGraphOperation(context, entity); 98 | } 99 | 100 | /// 101 | /// Create properties collection which will be need to define model matches 102 | /// 103 | /// Entity type 104 | /// Context instance 105 | /// Entity instance 106 | /// Information about finded properties will be sent to each function recursively 107 | /// ExtendedPropertyInfo collection which holds property information and their values 108 | internal static IEnumerable GetPropertiesForFiltering( 109 | this DbContext context, 110 | TSource model, 111 | IEnumerable properties) 112 | where TSource : class 113 | { 114 | if (model == null) 115 | throw new ArgumentNullException("model"); 116 | 117 | if (context == null) 118 | throw new ArgumentNullException("context"); 119 | 120 | // Get primary key properties of the type alongside with the value 121 | var primaryKeyProperties = context.GetPrimaryKeys() 122 | .MakePropertiesExtended(model); 123 | 124 | // Get unique properterties of the type alongside with the value 125 | var uniqueKeyProperties = ExtendedEntityTypeConfiguration 126 | .GetInstance() 127 | .UniqueKeys 128 | .OfReflectingType() 129 | .MakePropertiesExtended(model); 130 | 131 | // If all primary key properties has non-default value 132 | if (primaryKeyProperties.Any() && !primaryKeyProperties.Select(prop => prop.PropertyValue).AnyDefault()) 133 | { 134 | properties = properties.Concat(primaryKeyProperties); 135 | } 136 | else if (uniqueKeyProperties.Any()) 137 | { 138 | properties = properties 139 | .Concat(uniqueKeyProperties.Where(prop => prop.PropertyInfo.PropertyType.IsBuiltInType())); 140 | 141 | // Find complex property 142 | var complexProperty = uniqueKeyProperties 143 | .Where(prop => !prop.PropertyInfo.PropertyType.IsBuiltInType()) 144 | .FirstOrDefault(prop => !prop.PropertyValue.IsDefault()); 145 | 146 | // Recursively get its properties 147 | if (complexProperty != null) 148 | { 149 | properties = properties.Add(complexProperty) 150 | .ToList(); 151 | 152 | properties = DbContextExtensions.GetPropertiesForFiltering( 153 | context, 154 | complexProperty.PropertyValue as dynamic, 155 | properties); 156 | } 157 | } 158 | 159 | return properties; 160 | } 161 | 162 | /// 163 | /// Define state of all entities based on their conditions 164 | /// which will be define automatically with the help of primary and uniquq keys. 165 | /// Also, detach duplicate entities. 166 | /// 167 | /// Entity type 168 | /// DbContext instance 169 | /// Entity instance 170 | /// Full entity tree instance 171 | internal static void DefineState( 172 | this DbContext context, 173 | TEntity entity, 174 | List wholeEntityTree) 175 | where TEntity : class 176 | { 177 | if (typeof(TEntity).FullName.Contains("District")) 178 | { 179 | } 180 | 181 | if (entity == null) 182 | throw new ArgumentNullException("entity"); 183 | 184 | var currentEntry = context.Entry(entity); 185 | 186 | // Find mathcing entities in local 187 | var matchedEntitiesInContext = context.FindMatchingEntity(entity, true); 188 | 189 | // If there is any, then keep the last entity. 190 | // And detach other entities. 191 | // Change their references to the last one in their owners. 192 | if (matchedEntitiesInContext != null && matchedEntitiesInContext.Count() > 1) 193 | { 194 | matchedEntitiesInContext 195 | .Except(entity) 196 | .ToList() 197 | .ForEach(matchedEntity => 198 | { 199 | var matchedEntityTrees = wholeEntityTree 200 | .Where(x => x.Entity.Equals(matchedEntity)) 201 | .ToList(); 202 | 203 | var matchedEntityTree = wholeEntityTree 204 | .First(x => x.Entity.Equals(matchedEntity)); 205 | 206 | if (matchedEntityTree.Owner != null) 207 | { 208 | var property = matchedEntityTree.Owner.GetProperty(matchedEntity); 209 | 210 | if (property != null) 211 | { 212 | var matchedEntityPropertyState = context.Entry(matchedEntityTree.Owner) 213 | .Member(property.Name) 214 | .EntityEntry 215 | .State; 216 | 217 | context.Entry(matchedEntityTree.Owner).Member(property.Name).CurrentValue = entity; 218 | 219 | // Then detach it 220 | context.DetachEntityWithDependants(matchedEntity); 221 | 222 | // And then set new reference's state to unchanged for property fix-up. 223 | context.Entry(matchedEntityTree.Owner).Member(property.Name).EntityEntry.State = EntityState.Unchanged; 224 | context.Entry(matchedEntityTree.Owner).Member(property.Name).EntityEntry.State = EntityState.Added; 225 | } 226 | else 227 | // Then detach it 228 | context.DetachEntityWithDependants(matchedEntity); 229 | } 230 | else 231 | context.DetachEntityWithDependants(matchedEntity); 232 | }); 233 | } 234 | 235 | // Find mathing entities in the database 236 | var matchedEntitiesInDatabase = context.FindMatchingEntity(entity, false); 237 | var primaryKeysOfEntity = context.GetPrimaryKeys() 238 | .MakePropertiesExtended(entity); 239 | 240 | if (matchedEntitiesInDatabase != null) 241 | { 242 | var matchedEntityInDatabase = matchedEntitiesInDatabase 243 | .AsNoTracking() 244 | .SingleOrDefault(); 245 | 246 | if (matchedEntityInDatabase != null) 247 | { 248 | // If it is already existed in the database, then set it's PKs 249 | foreach (var primaryKey in primaryKeysOfEntity) 250 | currentEntry.Property(primaryKey.PropertyInfo.Name).CurrentValue = 251 | matchedEntityInDatabase.GetValue(primaryKey.PropertyInfo.Name); 252 | 253 | // After this, set it's EntityState to Unchanged for allowing EF to fix properties. 254 | currentEntry.State = EntityState.Unchanged; 255 | 256 | // And then check, if it has different values than original entity in the database. 257 | currentEntry.CompareWith(matchedEntityInDatabase); 258 | } 259 | } 260 | 261 | // If that entity has PK already with non-default values, 262 | // then change it's state to Unchanged for property fix-up 263 | // and then back to Added. 264 | if (currentEntry.State == EntityState.Added && !primaryKeysOfEntity.Select(prop => prop.PropertyValue).AnyDefault()) 265 | { 266 | currentEntry.State = EntityState.Unchanged; 267 | currentEntry.State = EntityState.Added; 268 | } 269 | } 270 | 271 | /// 272 | /// Will compare current entity with original one, and set modified properties as Modified 273 | /// and will consider InsertOnly and UpdateOnly properties also. 274 | /// 275 | /// Entity type 276 | /// Entry of the current entity 277 | /// Original values of this entity in the database which will be compared with current entity 278 | internal static void CompareWith( 279 | this DbEntityEntry currentEntry, 280 | TEntity matchedEntityInDatabase) 281 | where TEntity : class 282 | { 283 | // But firstly, get collection of properties which will never be updated. 284 | var notModifiableProperties = ExtendedEntityTypeConfiguration 285 | .GetInstance() 286 | .NotCompareProperties 287 | .OfReflectingType() 288 | .Select(prop => prop.Name); 289 | 290 | // Collection of properties which must be compared. 291 | var comparableProperties = currentEntry.CurrentValues 292 | .PropertyNames 293 | .Except(notModifiableProperties); 294 | 295 | foreach (var propertyName in comparableProperties) 296 | { 297 | var original = matchedEntityInDatabase.GetValue(propertyName); 298 | var current = currentEntry.Entity.GetValue(propertyName); 299 | 300 | if (!Compare.IsEqual(original, current)) 301 | { 302 | currentEntry.Property(propertyName).IsModified = true; 303 | } 304 | } 305 | } 306 | 307 | /// 308 | /// Detach entity alongside with all it's dependants recursively. 309 | /// 310 | /// Entity type 311 | /// DbContext instance 312 | /// Entity instance 313 | internal static void DetachEntityWithDependants( 314 | this DbContext context, 315 | TEntity entity) 316 | where TEntity : class 317 | { 318 | var dependants = context.GetRelatedEntities() 319 | .Where(prop => prop.EntityRelationDirection == EntityRelationDirection.Dependant); 320 | 321 | foreach (var dependant in dependants) 322 | { 323 | var dependantValue = entity.GetValue(dependant.PropertyInfo); 324 | 325 | // If property is collection, then execute process for each elements. 326 | if (dependant.PropertyInfo.PropertyType.IsGenericCollection()) 327 | { 328 | var collection = dependantValue as IEnumerable; 329 | 330 | if (collection == null) 331 | continue; 332 | 333 | foreach (var collectionElement in collection.ToList()) 334 | { 335 | if (collectionElement == null) 336 | continue; 337 | 338 | DetachEntityWithDependants( 339 | context, 340 | collectionElement as dynamic); 341 | } 342 | } 343 | else 344 | { 345 | if (dependantValue == null) 346 | continue; 347 | 348 | DetachEntityWithDependants(context, 349 | dependantValue as dynamic); 350 | } 351 | } 352 | 353 | context.Entry(entity).State = EntityState.Detached; 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /EntityGraphOperations/EntityGraphOperations.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {866B4EB2-C38F-48D6-BB89-84DE03733638} 8 | Library 9 | Properties 10 | EntityGraphOperations 11 | EntityGraphOperations 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll 36 | True 37 | 38 | 39 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll 40 | True 41 | 42 | 43 | False 44 | ..\lib\net45\Extensions.General.dll 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Compare.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Extensions.General; 3 | 4 | namespace EntityGraphOperations.Internal 5 | { 6 | internal static class Compare 7 | { 8 | /// 9 | /// Compare all type of objects. 10 | /// 11 | /// This method was created for comapring properties of entity objects in runtime. 12 | /// First object instance 13 | /// Second object instance 14 | /// True, if their value is same; otherwise, false 15 | internal static bool IsEqual(object obj1, object obj2) 16 | { 17 | if (obj1 == null && obj2 == null) 18 | return true; 19 | 20 | if (obj1 is string || obj2 is string) 21 | { 22 | return Equals(obj1 as string ?? "", obj2 as string ?? ""); 23 | } 24 | 25 | if (obj1 == null && obj2 != null) 26 | return false; 27 | 28 | if (obj1 != null && obj2 == null) 29 | return false; 30 | 31 | if (obj1.GetType().IsCollection() && obj1.GetType().IsCollection()) 32 | { 33 | return Enumerable.SequenceEqual(obj1 as dynamic, obj2 as dynamic); 34 | } 35 | 36 | return Equals(obj1, obj2); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/HelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using Extensions.General; 7 | using System.Data.Entity.Infrastructure; 8 | using System.Data.Entity; 9 | using System.Data.Entity.Core.Objects; 10 | using System.Data.Entity.Core.Metadata.Edm; 11 | using EntityGraphOperations.Internal.Models; 12 | using System.Data; 13 | 14 | namespace EntityGraphOperations.Internal 15 | { 16 | internal static class HelperExtensions 17 | { 18 | /// 19 | /// Filters a sequence of values based on specified properties of a specified model. 20 | /// 21 | /// Model type 22 | /// An System.Linq.IQueryable to filter 23 | /// Model instance 24 | /// Specified properties of a model 25 | /// 26 | /// An System.Linq.IQueryable that contains elements from the input sequence 27 | /// that satisfy the condition specified by generated predicate. 28 | /// 29 | internal static IQueryable Where(this IQueryable source, 30 | IEnumerable properties) 31 | { 32 | if (source == null) 33 | throw new ArgumentNullException("source"); 34 | 35 | if (properties == null) 36 | throw new ArgumentNullException("properties"); 37 | 38 | if (!properties.Any()) 39 | throw new InvalidOperationException("There is not any properties for building expression."); 40 | 41 | var parameter = Expression.Parameter(typeof(TSource), "model"); 42 | var propertyOwnerParameter = parameter as Expression; 43 | Expression finalExpression = null; 44 | 45 | foreach (var prop in properties) 46 | { 47 | if (!prop.PropertyInfo.PropertyType.IsBuiltInType()) 48 | { 49 | propertyOwnerParameter = Expression.Property(propertyOwnerParameter, prop.PropertyInfo); 50 | continue; 51 | } 52 | 53 | if (propertyOwnerParameter is MemberExpression) 54 | { 55 | if (finalExpression == null) 56 | finalExpression = Expression.NotEqual(propertyOwnerParameter, 57 | Expression.Constant(null)); 58 | else 59 | finalExpression = Expression.AndAlso(finalExpression, 60 | Expression.NotEqual(propertyOwnerParameter, Expression.Constant(null))); 61 | } 62 | 63 | var leftExpression = Expression.Property(propertyOwnerParameter, prop.PropertyInfo.Name); 64 | var rightExpression = Expression.Constant(prop.PropertyValue); 65 | 66 | // In case of nullable type, it will help us 67 | var converted = Expression.Convert(rightExpression, leftExpression.Type); 68 | 69 | // model => model.Property == 45 (f.e) 70 | var comparison = Expression.Equal(leftExpression, converted); 71 | 72 | if (finalExpression == null) 73 | finalExpression = comparison; 74 | else 75 | finalExpression = Expression.AndAlso(finalExpression, comparison); 76 | } 77 | 78 | 79 | var resultExpression = Expression.Lambda>(finalExpression, parameter); 80 | return source.Where(resultExpression); 81 | } 82 | 83 | /// 84 | /// Get foreign key properties between two types. 85 | /// 86 | /// Principal end of the association 87 | /// Dependant end of the association 88 | /// DbContext instance 89 | /// Properties of the TDependantEntity type, which associates it with the TPrincipalEntity type. 90 | internal static ForeignKeyRelationship GetForeignKeyRelationship(this DbContext context) 91 | where TPrincipalEntity : class 92 | where TDependantEntity : class 93 | { 94 | ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; 95 | ObjectSet set = objectContext.CreateObjectSet(); 96 | 97 | foreach (NavigationProperty entityMember in set.EntitySet.ElementType.NavigationProperties) 98 | { 99 | if (entityMember.TypeUsage.EdmType.Name == typeof(TPrincipalEntity).Name) 100 | { 101 | var dependantProperties = entityMember 102 | .GetDependentProperties() 103 | .Select(x => typeof(TDependantEntity).GetProperty(x.Name)); 104 | 105 | var principalProperties = context.GetPrimaryKeys(); 106 | 107 | return new ForeignKeyRelationship(principalProperties, dependantProperties); 108 | } 109 | } 110 | 111 | return null; 112 | } 113 | 114 | /// 115 | /// Get primary key properties for the specified entity type 116 | /// 117 | /// Entity type 118 | /// Context instance 119 | /// PropertInfo collection of primary key properties 120 | internal static IEnumerable GetPrimaryKeys(this DbContext context) 121 | where TEntity : class 122 | { 123 | ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; 124 | ObjectSet set = objectContext.CreateObjectSet(); 125 | 126 | return set.EntitySet 127 | .ElementType 128 | .KeyMembers 129 | .Select(key => typeof(TEntity).GetProperty(key.Name)); 130 | } 131 | 132 | /// 133 | /// Get navigational properties for the specified entity type 134 | /// 135 | /// Entity type 136 | /// Context instance 137 | /// PropertInfo collection of navigational properties 138 | internal static IEnumerable GetNavigationalProperties(this DbContext context) 139 | where TEntity : class 140 | { 141 | ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; 142 | ObjectSet set = objectContext.CreateObjectSet(); 143 | 144 | foreach (var navigationalProperty in set.EntitySet.ElementType.NavigationProperties) 145 | { 146 | yield return typeof(TEntity) 147 | .GetProperty(navigationalProperty.Name); 148 | } 149 | } 150 | 151 | /// 152 | /// Get navigation properties which is dependant or principal of the specified entity 153 | /// 154 | /// Entity type 155 | /// Context instance 156 | /// Collection of EntityRelatedProperties which is dependant or principal of the specified entity 157 | internal static IEnumerable GetRelatedEntities(this DbContext context) 158 | where TEntity : class 159 | { 160 | ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; 161 | 162 | // All navigation properties types 163 | var toManyDependantTypeNames = context.GetNavigationalProperties() 164 | .Select(x => x.PropertyType.Name); 165 | 166 | // Dependant types 167 | var dependantTypeNames = objectContext.MetadataWorkspace 168 | .GetItems(DataSpace.SSpace) 169 | .Where(x => x.ReferentialConstraints[0].FromRole.Name == typeof(TEntity).Name) 170 | .Select(x => x.ReferentialConstraints[0].ToRole.Name); 171 | 172 | // Principal types 173 | var principalTypeNames = objectContext.MetadataWorkspace 174 | .GetItems(DataSpace.SSpace) 175 | .Where(x => x.ReferentialConstraints[0].ToRole.Name == typeof(TEntity).Name) 176 | .Select(x => x.ReferentialConstraints[0].FromRole.Name); 177 | 178 | var dependantTypeNamesIncludedCloudTables = dependantTypeNames 179 | .Union(toManyDependantTypeNames) 180 | .Except(principalTypeNames); 181 | 182 | var dependantProperties = typeof(TEntity) 183 | .GetPropertiesWithTypeNames(dependantTypeNamesIncludedCloudTables) 184 | .Select(prop => new EntityRelatedProperties 185 | { 186 | EntityRelationDirection = EntityRelationDirection.Dependant, 187 | PropertyInfo = prop 188 | }); 189 | 190 | var principalProperties = typeof(TEntity) 191 | .GetPropertiesWithTypeNames(principalTypeNames) 192 | .Select(prop => new EntityRelatedProperties 193 | { 194 | EntityRelationDirection = EntityRelationDirection.Principal, 195 | PropertyInfo = prop 196 | }); 197 | 198 | return dependantProperties 199 | .Concat(principalProperties); 200 | } 201 | 202 | /// 203 | /// Returns new ExtendedPropertyInfo collection based on object instance and it's properties 204 | /// 205 | /// Type of the model which owns properties 206 | /// Properties collection 207 | /// Model instance 208 | /// new ExtendedPropertyInfo collection based on object instance and it's properties 209 | internal static IEnumerable MakePropertiesExtended(this IEnumerable properties, 210 | TSource model) 211 | where TSource : class 212 | { 213 | if (model == null) 214 | throw new ArgumentNullException("model"); 215 | 216 | if (properties == null) 217 | throw new ArgumentNullException("properties"); 218 | 219 | foreach (var property in properties) 220 | { 221 | yield return new ExtendedPropertyInfo 222 | { 223 | PropertyInfo = property, 224 | PropertyValue = model.GetValue(property) 225 | }; 226 | } 227 | } 228 | 229 | /// 230 | /// Construct entity tree list for the specified entity 231 | /// 232 | /// Entity type 233 | /// Context instance 234 | /// Entity instance 235 | /// Entity's grand owner instance 236 | /// Entity's owner instance 237 | /// Will help us to get all entity trees recursively 238 | /// Holds all dependant and principal objects and owner of the entity and it's all child entities recursively 239 | internal static List GetEntityTree(this DbContext context, 240 | TEntity entity, 241 | object grandOwner, 242 | object owner, 243 | List lastRecursionReturn) 244 | where TEntity : class 245 | { 246 | // Iterate over dependant and principal navigational properties 247 | foreach (var relatedProperty in context.GetRelatedEntities()) 248 | { 249 | var relatedPropertyValue = entity.GetValue(relatedProperty.PropertyInfo); 250 | 251 | if (relatedPropertyValue == null) 252 | continue; 253 | 254 | // Use loop to check each element of the collection 255 | if (relatedProperty.PropertyInfo.PropertyType.IsGenericCollection()) 256 | { 257 | (relatedPropertyValue as IEnumerable) 258 | .ToList() 259 | .ForEach(element => 260 | { 261 | if (element == null) 262 | return; 263 | 264 | var isResursionNeeded = CheckAndInsertIntoTreeList(element, 265 | entity, 266 | owner, 267 | relatedProperty.EntityRelationDirection, 268 | lastRecursionReturn); 269 | 270 | if (isResursionNeeded) 271 | GetEntityTree(context, element as dynamic, owner, entity, lastRecursionReturn); 272 | }); 273 | } 274 | else 275 | { 276 | var isResursionNeeded = CheckAndInsertIntoTreeList(relatedPropertyValue, 277 | entity, 278 | owner, 279 | relatedProperty.EntityRelationDirection, 280 | lastRecursionReturn); 281 | 282 | if (isResursionNeeded) 283 | GetEntityTree(context, relatedPropertyValue as dynamic, owner, entity, lastRecursionReturn); 284 | } 285 | 286 | } 287 | 288 | return lastRecursionReturn; 289 | } 290 | 291 | /// 292 | /// Check and insert new EntityTree to the list. 293 | /// Set it's dependant and principal entities. 294 | /// 295 | /// First side entity instance 296 | /// Second side entity instance 297 | /// Second side entity's owner instance 298 | /// How does first entity depends on second entity? Eitehr Principal or Dependant 299 | /// Holds all dependant and principal objects and owner of the entity and it's all child entities recursively 300 | /// 301 | /// If the first entity was not in the list, true; otherwise, false. 302 | /// The first entities properties will be checked in case of True. 303 | /// 304 | private static bool CheckAndInsertIntoTreeList( 305 | object firstEntity, 306 | object secondEntity, 307 | object secondEntityOwner, 308 | EntityRelationDirection entityRelationDirection, 309 | List lastRecursionReturn) 310 | { 311 | var firstEntityTree = lastRecursionReturn.FirstOrDefault(x => x.Entity.Equals(firstEntity)); 312 | var secondEntityTree = lastRecursionReturn.FirstOrDefault(x => x.Entity.Equals(secondEntity)); 313 | var isResursionNeeded = firstEntityTree == null; 314 | 315 | if (firstEntityTree == null) 316 | { 317 | firstEntityTree = new EntityTree(firstEntity, secondEntity); 318 | lastRecursionReturn.Add(firstEntityTree); 319 | } 320 | 321 | if (secondEntityTree == null) 322 | { 323 | secondEntityTree = new EntityTree(secondEntity, secondEntityOwner); 324 | lastRecursionReturn.Add(secondEntityTree); 325 | } 326 | 327 | if (entityRelationDirection == EntityRelationDirection.Dependant) 328 | { 329 | firstEntityTree.DirectPrincipals.Add(secondEntityTree); 330 | secondEntityTree.DirectDependants.Add(firstEntityTree); 331 | } 332 | else 333 | { 334 | firstEntityTree.DirectDependants.Add(secondEntityTree); 335 | secondEntityTree.DirectPrincipals.Add(firstEntityTree); 336 | } 337 | 338 | return isResursionNeeded; 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Models/EntityRelatedProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace EntityGraphOperations.Internal 4 | { 5 | /// 6 | /// Helper model which holds navigation property information 7 | /// and its role in relationship 8 | /// 9 | internal class EntityRelatedProperties 10 | { 11 | internal PropertyInfo PropertyInfo { get; set; } 12 | internal EntityRelationDirection EntityRelationDirection { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Models/EntityRelationDirection.cs: -------------------------------------------------------------------------------- 1 | namespace EntityGraphOperations.Internal 2 | { 3 | /// 4 | /// Entity's role in the relationship 5 | /// 6 | internal enum EntityRelationDirection 7 | { 8 | Principal, 9 | Dependant 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Models/EntityTree.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace EntityGraphOperations.Internal 5 | { 6 | /// 7 | /// Main model of the logic. 8 | /// Holds all dependant and principal objects and owner of the entity 9 | /// 10 | internal class EntityTree 11 | { 12 | internal object Entity { get; set; } 13 | internal HashSet DirectPrincipals { get; set; } 14 | internal HashSet DirectDependants { get; set; } 15 | internal object Owner { get; set; } 16 | 17 | #region Constructors 18 | internal EntityTree() 19 | { 20 | DirectPrincipals = new HashSet(); 21 | DirectDependants = new HashSet(); 22 | } 23 | 24 | internal EntityTree(object entity) 25 | : this() 26 | { 27 | this.Entity = entity; 28 | } 29 | 30 | internal EntityTree(object entity, object owner) 31 | : this(entity) 32 | { 33 | this.Owner = owner; 34 | } 35 | #endregion 36 | 37 | /// 38 | /// Get all principles recursively 39 | /// 40 | /// All entity trees list 41 | /// Collection of direct and indirect Principal entity's EntityTree objects 42 | internal IEnumerable GetIndirectPrinciples(List wholeTree) 43 | { 44 | foreach (var item in this.DirectPrincipals.Select(x => x)) 45 | { 46 | yield return item; 47 | } 48 | 49 | foreach (var item in this.DirectPrincipals.SelectMany(x => x.GetIndirectPrinciples(wholeTree))) 50 | { 51 | yield return item; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Models/ExtendedPropertyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace EntityGraphOperations.Internal 4 | { 5 | /// 6 | /// Model which holds property value inside it's owner alongside iwth their PropertyInfo's 7 | /// 8 | internal class ExtendedPropertyInfo 9 | { 10 | internal PropertyInfo PropertyInfo { get; set; } 11 | internal object PropertyValue { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EntityGraphOperations/Internal/Models/ForeignKeyRelationship.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace EntityGraphOperations.Internal.Models 5 | { 6 | public class ForeignKeyRelationship 7 | { 8 | public ForeignKeyRelationship( 9 | IEnumerable principalProperties, 10 | IEnumerable dependantProperties) 11 | { 12 | DependantProperties = dependantProperties; 13 | PrincipalProperties = principalProperties; 14 | } 15 | 16 | public IEnumerable DependantProperties { get; set; } 17 | public IEnumerable PrincipalProperties { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EntityGraphOperations/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("EntityGraphOperations")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("EntityGraphOperations")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("866b4eb2-c38f-48d6-bb89-84de03733638")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /EntityGraphOperations/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Farhad Jabiyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | EntityGraphOperations 5 | 1.0.0 6 | FarhadJabiyev 7 | FarhadJabiyev 8 | https://github.com/FarhadJabiyev/EntityGraphOperations 9 | https://github.com/FarhadJabiyev/EntityGraphOperations 10 | https://cdn0.iconfinder.com/data/icons/iVista2/128/New_Database.png 11 | false 12 | EntityGraphOperations for Entity Framework Code First automatically defines states of an entity graph. 13 | First Release. 14 | Copyright 2016 15 | EntityFramework EF CodeFirst EntityGraph EntityGraphOperations Fluent 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Contact me if you have any problem or question. (email: cebiyevferhad@hotmail.com) 2 | 3 | You can check the code-project article for reading and downloading a step-by-step sample project demonstration. (http://www.codeproject.com/Articles/1131322/Automatically-Define-the-State-of-All-Entities-in) 4 | 5 | ### NuGet Packages 6 | 7 | ``` 8 | Install-Package EntityGraphOperations 9 | ``` 10 | 11 | # The problem – Tons of repetitive code segments 12 | 13 | Usually we find ourselves writing very similar codes for defining the state of entities. Normally, the procedure is as follows: 14 | 15 | - Determine which properties are needed for defining existence of the entity in the database (this could be primary key properties or unique key properties). 16 | - If the result is null then the entity must be inserted. 17 | - If the result is not null and if a change has occurred in any part of the entity, then the entity must be updated. 18 | - If we have a collection of entities, then we need to compare it with the ones in the database and delete those which do not exist in the collection anymore. 19 | 20 | and so on … 21 | 22 | ### Additional explanation about why sometimes we need unique key properties in addition to the primary key properties 23 | 24 | Say we have Phone entity which has some properties: 25 | ID, 26 | Digits, 27 | Prefix 28 | … 29 | 30 | ID is auto-generated primary key. We do not want to insert same phone number to the table with a different ID. So Digits and Prefix properties are unique together. This situation is forcing us to take into consideration the followings: 31 | 32 | If Id is 0, and there is not any corresponding entity in the database with the specified Digits and prefix, then it must be inserted. Otherwise, if a change has occurred, then it must be updated and so on… 33 | 34 | Now, let’s do the same things all over again for a different entity graph. Again, and again… 35 | 36 | # The solution – Use EntityGraphOperations for Entity Framework Code First 37 | 38 | ### Features: 39 | - Automatically define state of all entities 40 | - Update only those entities which have changed 41 | - Fluent API style mapping of special entity configurations 42 | - Let the user manually manage graph after automatically determining state of all entities 43 | 44 | ### The example: 45 | Let’s say I have got a Person object. Person could have many phones, a Document and he/she could have a spouse. 46 | 47 | ```csharp 48 | public class Person 49 | { 50 | public int Id { get; set; } 51 | public string FirstName { get; set; } 52 | public string LastName { get; set; } 53 | public string MiddleName { get; set; } 54 | public int Age { get; set; } 55 | public int DocumentId {get; set;} 56 | 57 | public virtual ICollection Phones { get; set; } 58 | public virtual Document Document { get; set; } 59 | public virtual PersonSpouse PersonSpouse { get; set; } 60 | } 61 | ``` 62 | 63 | I want to determine the state of all entities which are included in the graph. 64 | ```csharp 65 | context.InsertOrUpdateGraph(person) 66 | .After(entity => 67 | { 68 | // Delete missing phones. 69 | entity.HasCollection(p => p.Phones) 70 | .DeleteMissingEntities(); 71 | 72 | // Delete if spouse is not exist anymore. 73 | entity.HasNavigationalProperty(m => m.PersonSpouse) 74 | .DeleteIfNull(); 75 | }); 76 | ``` 77 | Also as you remember unique key properties could play role while defining the state of Phone entity. For such special purposes we have `ExtendedEntityTypeConfiguration<>` class, which inherits from `EntityTypeConfiguration<>`. If we want to use such special configurations then we must inherit our mapping classes from `ExtendedEntityTypeConfiguration<>`, rather than `EntityTypeConfiguration<>`. For example: 78 | 79 | ```csharp 80 | public class PhoneMap: ExtendedEntityTypeConfiguration 81 | { 82 | public PhoneMap() 83 | { 84 | // Primary Key 85 | this.HasKey(m => m.Id); 86 | … 87 | // Unique keys 88 | this.HasUniqueKey(m => new { m.Prefix, m.Digits }); 89 | } 90 | } 91 | ``` 92 | That’s all. 93 | -------------------------------------------------------------------------------- /lib/net45/EntityFramework.SqlServer.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FarhadJabiyev/EntityGraphOperations/35b69f39ac3524a7c9fb301a0b5b7ddd04d1437d/lib/net45/EntityFramework.SqlServer.dll -------------------------------------------------------------------------------- /lib/net45/EntityFramework.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FarhadJabiyev/EntityGraphOperations/35b69f39ac3524a7c9fb301a0b5b7ddd04d1437d/lib/net45/EntityFramework.dll -------------------------------------------------------------------------------- /lib/net45/EntityGraphOperations.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FarhadJabiyev/EntityGraphOperations/35b69f39ac3524a7c9fb301a0b5b7ddd04d1437d/lib/net45/EntityGraphOperations.dll -------------------------------------------------------------------------------- /lib/net45/Extensions.General.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FarhadJabiyev/EntityGraphOperations/35b69f39ac3524a7c9fb301a0b5b7ddd04d1437d/lib/net45/Extensions.General.dll -------------------------------------------------------------------------------- /nuget.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FarhadJabiyev/EntityGraphOperations/35b69f39ac3524a7c9fb301a0b5b7ddd04d1437d/nuget.exe --------------------------------------------------------------------------------