Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows to merge on a custom entity key instead of primary key #74

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion GraphDiff/GraphDiff.Tests/Models/TestModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ namespace RefactorThis.GraphDiff.Tests.Models
public class Entity
{
[Key]
public int Id { get; set; }
public int Id { get; set; }

public Guid UniqueId { get; set; }

[MaxLength(128)]
public string Title { get; set; }
Expand Down
40 changes: 40 additions & 0 deletions GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Data.Entity;
using System.Collections.Generic;
using System;

namespace RefactorThis.GraphDiff.Tests.Tests
{
Expand Down Expand Up @@ -341,5 +342,44 @@ public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds()
Assert.IsTrue(list[3].Title == "Finish");
}
}

[TestMethod]
public void ShouldUpdateItemInOwnedCollectionWithCustomKey()
{
var node1 = new TestNode
{
Title = "New Node",
OneToManyOwned = new List<OneToManyOwnedModel>
{
new OneToManyOwnedModel { Title = "Hello", UniqueId = new Guid("DA6B78FF-BB7F-4FA1-8659-F64AC6457D14") }
}
};

int originalOwnedId;
using (var context = new TestDbContext())
{
context.Nodes.Add(node1);
context.SaveChanges();
originalOwnedId = node1.OneToManyOwned.First().Id;
} // Simulate detach

node1.OneToManyOwned.First().Title = "What's up";
node1.OneToManyOwned.First().Id = 0; //We will try to update on Guid

using (var context = new TestDbContext())
{
// Setup mapping
context.UpdateGraph(node1, map => map
.OwnedCollection(p => p.OneToManyOwned),
keysConfiguration: new KeysConfiguration()
.ForEntity<OneToManyOwnedModel>(e => e.UniqueId));

context.SaveChanges();
var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id);
Assert.IsNotNull(node2);
var owned = node2.OneToManyOwned.First();
Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up" && owned.Id == originalOwnedId);
}
}
}
}
24 changes: 12 additions & 12 deletions GraphDiff/GraphDiff/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
using RefactorThis.GraphDiff.Internal;
using RefactorThis.GraphDiff.Internal.Caching;
using RefactorThis.GraphDiff.Internal.Graph;
using RefactorThis.GraphDiff.Internal.GraphBuilders;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace RefactorThis.GraphDiff
Expand All @@ -26,10 +23,11 @@ public static class DbContextExtensions
/// <param name="entity">The root entity.</param>
/// <param name="mapping">The mapping configuration to define the bounds of the graph</param>
/// <param name="updateParams">Update configuration overrides</param>
/// <param name="keysConfiguration">The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given.</param>
/// <returns>The attached entity graph</returns>
public static T UpdateGraph<T>(this DbContext context, T entity, Expression<Func<IUpdateConfiguration<T>, object>> mapping, UpdateParams updateParams = null) where T : class, new()
public static T UpdateGraph<T>(this DbContext context, T entity, Expression<Func<IUpdateConfiguration<T>, object>> mapping, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new()
{
return UpdateGraph<T>(context, entity, mapping, null, updateParams);
return UpdateGraph<T>(context, entity, mapping, null, updateParams, keysConfiguration);
}

/// <summary>
Expand All @@ -40,10 +38,11 @@ public static T UpdateGraph<T>(this DbContext context, T entity, Expression<Func
/// <param name="entity">The root entity.</param>
/// <param name="mappingScheme">Pre-configured mappingScheme</param>
/// <param name="updateParams">Update configuration overrides</param>
/// <param name="keysConfiguration">The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given.</param>
/// <returns>The attached entity graph</returns>
public static T UpdateGraph<T>(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null) where T : class, new()
public static T UpdateGraph<T>(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new()
{
return UpdateGraph<T>(context, entity, null, mappingScheme, updateParams);
return UpdateGraph<T>(context, entity, null, mappingScheme, updateParams, keysConfiguration);
}

/// <summary>
Expand All @@ -53,10 +52,11 @@ public static T UpdateGraph<T>(this DbContext context, T entity, string mappingS
/// <param name="context">The database context to attach / detach.</param>
/// <param name="entity">The root entity.</param>
/// <param name="updateParams">Update configuration overrides</param>
/// <param name="keysConfiguration">The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given.</param>
/// <returns>The attached entity graph</returns>
public static T UpdateGraph<T>(this DbContext context, T entity, UpdateParams updateParams = null) where T : class, new()
public static T UpdateGraph<T>(this DbContext context, T entity, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new()
{
return UpdateGraph<T>(context, entity, null, null, updateParams);
return UpdateGraph<T>(context, entity, null, null, updateParams, keysConfiguration);
}

/// <summary>
Expand All @@ -69,7 +69,7 @@ public static T UpdateGraph<T>(this DbContext context, T entity, UpdateParams up
/// <returns>The aggregate loaded from the database</returns>
public static T LoadAggregate<T>(this DbContext context, Func<T, bool> keyPredicate, QueryMode queryMode = QueryMode.SingleQuery) where T : class
{
var entityManager = new EntityManager(context);
var entityManager = new EntityManager(context, new KeysConfiguration());
var graph = new AggregateRegister(new CacheProvider()).GetEntityGraph<T>();
var queryLoader = new QueryLoader(context, entityManager);

Expand All @@ -85,12 +85,12 @@ public static T UpdateGraph<T>(this DbContext context, T entity, UpdateParams up

// other methods are convenience wrappers around this.
private static T UpdateGraph<T>(this DbContext context, T entity, Expression<Func<IUpdateConfiguration<T>, object>> mapping,
string mappingScheme, UpdateParams updateParams) where T : class, new()
string mappingScheme, UpdateParams updateParams, KeysConfiguration keysConfiguration) where T : class, new()
{
GraphNode root;
GraphDiffer<T> differ;

var entityManager = new EntityManager(context);
var entityManager = new EntityManager(context, keysConfiguration ?? new KeysConfiguration());
var queryLoader = new QueryLoader(context, entityManager);
var register = new AggregateRegister(new CacheProvider());

Expand Down
1 change: 1 addition & 0 deletions GraphDiff/GraphDiff/GraphDiff.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<Compile Include="Internal\GraphBuilders\ConfigurationGraphBuilder.cs" />
<Compile Include="Internal\QueryLoader.cs" />
<Compile Include="IUpdateConfiguration.cs" />
<Compile Include="KeysConfiguration.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="DbContextExtensions.cs" />
<Compile Include="QueryMode.cs" />
Expand Down
24 changes: 19 additions & 5 deletions GraphDiff/GraphDiff/Internal/ChangeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,24 @@ public EntityState GetItemState(object item)

public void UpdateItem(object from, object to, bool doConcurrencyCheck = false)
{
if (doConcurrencyCheck && _context.Entry(to).State != EntityState.Added)
Type entityType = from.GetType();
var toEntry = _context.Entry(to);

if (doConcurrencyCheck && toEntry.State != EntityState.Added)
{
EnsureConcurrency(entityType, from, to);
}

var metadata = _objectContext.MetadataWorkspace
.GetItems<EntityType>(DataSpace.OSpace)
.SingleOrDefault(p => p.FullName == entityType.FullName);

// When a custom key is specified the primary key in the from object is ignored.
// We must set it to the actual value from database so it won't try to change the primary key
if (_entityManager.KeysConfiguration.HasConfigurationFor(entityType))
{
EnsureConcurrency(from, to);
// Copy inverted for primary key : from context entity to detached entity
_entityManager.CopyPrimaryKeyFields(entityType, from: to, to: from);
}

_context.Entry(to).CurrentValues.SetValues(from);
Expand Down Expand Up @@ -171,10 +186,9 @@ public void AttachRequiredNavigationProperties(object updating, object persisted

// Privates

private void EnsureConcurrency(object entity1, object entity2)
private void EnsureConcurrency(Type entityType, object entity1, object entity2)
{
// get concurrency properties of T
var entityType = ObjectContext.GetObjectType(entity1.GetType());
var metadata = _objectContext.MetadataWorkspace;

var objType = metadata.GetItems<EntityType>(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName);
Expand Down Expand Up @@ -222,7 +236,7 @@ private object FindTrackedEntity(object entity)
private object FindEntityByKey(object associatedEntity)
{
var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType());
var keyFields = _entityManager.GetPrimaryKeyFieldsFor(associatedEntityType);
var keyFields = _entityManager.GetKeyFieldsFor(associatedEntityType);
var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray();
return _context.Set(associatedEntityType).Find(keys);
}
Expand Down
75 changes: 64 additions & 11 deletions GraphDiff/GraphDiff/Internal/EntityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ namespace RefactorThis.GraphDiff.Internal
/// </summary>
internal interface IEntityManager
{
/// <summary>
/// Gets custom key mappins for entities
/// </summary>
KeysConfiguration KeysConfiguration { get; }

/// <summary>
/// Creates the unique entity key for an entity
/// </summary>
Expand All @@ -31,9 +36,14 @@ internal interface IEntityManager
bool AreKeysIdentical(object entity1, object entity2);

/// <summary>
/// Returns the primary key fields for a given entity type
/// Returns the key fields (using key configuration if available) for a given entity type
/// </summary>
IEnumerable<PropertyInfo> GetKeyFieldsFor(Type entityType);

/// <summary>
/// Copy primary key fields from an entity to another of the same type
/// </summary>
IEnumerable<PropertyInfo> GetPrimaryKeyFieldsFor(Type entityType);
void CopyPrimaryKeyFields(Type entityType, object from, object to);

/// <summary>
/// Retrieves the required navigation properties for the given type
Expand All @@ -45,18 +55,27 @@ internal interface IEntityManager
/// </summary>
IEnumerable<NavigationProperty> GetNavigationPropertiesForType(Type entityType);
}

internal class EntityManager : IEntityManager
{
private readonly DbContext _context;

private ObjectContext _objectContext
{
get { return ((IObjectContextAdapter)_context).ObjectContext; }
}

public EntityManager(DbContext context)
public KeysConfiguration KeysConfiguration { get; private set; }

public EntityManager(DbContext context, KeysConfiguration keysConfiguration)
{
if (context == null)
throw new ArgumentNullException("context");
if (keysConfiguration == null)
throw new ArgumentNullException("keysConfiguration");

_context = context;
KeysConfiguration = keysConfiguration;
}

public EntityKey CreateEntityKey(object entity)
Expand All @@ -66,7 +85,18 @@ public EntityKey CreateEntityKey(object entity)
throw new ArgumentNullException("entity");
}

return _objectContext.CreateEntityKey(GetEntitySetName(entity.GetType()), entity);
var entityType = entity.GetType();
var entitySetName = GetEntitySetName(entityType);
if (KeysConfiguration.HasConfigurationFor(entityType))
{
var keyMembers = GetKeyFieldsFor(entityType)
.Select(p => new EntityKeyMember(p.Name, p.GetValue(entity, null)));
return new EntityKey(_objectContext.DefaultContainerName + "." + entitySetName, keyMembers);
}
else
{
return _objectContext.CreateEntityKey(entitySetName, entity);
}
}

public bool AreKeysIdentical(object newValue, object dbValue)
Expand All @@ -81,16 +111,30 @@ public bool AreKeysIdentical(object newValue, object dbValue)

public object CreateEmptyEntityWithKey(object entity)
{
var instance = Activator.CreateInstance(entity.GetType());
CopyPrimaryKeyFields(entity, instance);
var entityType = entity.GetType();
var instance = Activator.CreateInstance(entityType);
CopyKeyFields(entityType, entity, instance);
return instance;
}

public IEnumerable<PropertyInfo> GetKeyFieldsFor(Type entityType)
{
var keyColumns = KeysConfiguration.GetEntityKey(entityType);
if (keyColumns != null)
{
return keyColumns;
}
else
{
return GetPrimaryKeyFieldsFor(entityType);
}
}

public IEnumerable<PropertyInfo> GetPrimaryKeyFieldsFor(Type entityType)
{
var metadata = _objectContext.MetadataWorkspace
.GetItems<EntityType>(DataSpace.OSpace)
.SingleOrDefault(p => p.FullName == entityType.FullName);
.GetItems<EntityType>(DataSpace.OSpace)
.SingleOrDefault(p => p.FullName == entityType.FullName);

if (metadata == null)
{
Expand Down Expand Up @@ -134,9 +178,18 @@ private string GetEntitySetName(Type entityType)
return set != null ? set.Name : null;
}

private void CopyPrimaryKeyFields(object from, object to)
private void CopyKeyFields(Type entityType, object from, object to)
{
var keyProperties = GetKeyFieldsFor(entityType);
foreach (var keyProperty in keyProperties)
{
keyProperty.SetValue(to, keyProperty.GetValue(from, null), null);
}
}

public void CopyPrimaryKeyFields(Type entityType, object from, object to)
{
var keyProperties = GetPrimaryKeyFieldsFor(from.GetType());
var keyProperties = GetPrimaryKeyFieldsFor(entityType);
foreach (var keyProperty in keyProperties)
{
keyProperty.SetValue(to, keyProperty.GetValue(from, null), null);
Expand Down
4 changes: 2 additions & 2 deletions GraphDiff/GraphDiff/Internal/GraphDiffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery)
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
var entityManager = new EntityManager(_dbContext);
// Perform recursive update
var entityManager = new EntityManager(_dbContext, _entityManager.KeysConfiguration);
var changeTracker = new ChangeTracker(_dbContext, entityManager);
_root.Update(changeTracker, entityManager, persisted, updating);

Expand Down
2 changes: 1 addition & 1 deletion GraphDiff/GraphDiff/Internal/QueryLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public QueryLoader(DbContext context, IEntityManager entityManager)
private Func<T, bool> CreateKeyPredicateExpression<T>(IObjectContextAdapter context, T entity)
{
// get key properties of T
var keyProperties = _entityManager.GetPrimaryKeyFieldsFor(typeof(T)).ToList();
var keyProperties = _entityManager.GetKeyFieldsFor(typeof(T)).ToList();

ParameterExpression parameter = Expression.Parameter(typeof(T));
Expression expression = CreateEqualsExpression(entity, keyProperties[0], parameter);
Expand Down
Loading