Skip to content

Commit

Permalink
Space-age composite polymer; not a shred of sqlmetal... (Support comp…
Browse files Browse the repository at this point in the history
…osite keys end-to-end)

This change adds support for value generation with composite keys which was blocking use of composite keys end-to-end. The state entry will now generate values for any property marked to have value generation on add regardless of whether that property is part of a key, composite or not.
  • Loading branch information
ajcvickers committed May 28, 2014
1 parent 4f16276 commit 1a376a0
Show file tree
Hide file tree
Showing 10 changed files with 3,702 additions and 13,662 deletions.
81 changes: 56 additions & 25 deletions src/Microsoft.Data.Entity/ChangeTracking/StateEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Data.Entity.Identity;
using Microsoft.Data.Entity.Infrastructure;
using Microsoft.Data.Entity.Metadata;
using Microsoft.Data.Entity.Utilities;
Expand Down Expand Up @@ -118,66 +120,95 @@ public virtual void RemoveSidecar([NotNull] string name)

private void SetEntityState(EntityState entityState)
{
// TODO: Decide what to do here when we decide what to do with sync/async paths in general
var generatedValue = entityState == EntityState.Added && EntityState != EntityState.Added
? GenerateKeyValue().Result
: null;

SetEntityState(entityState, generatedValue);
SetEntityState(entityState, GenerateValues(GetValueGenerators(entityState)));
}

public virtual async Task SetEntityStateAsync(
EntityState entityState, CancellationToken cancellationToken = default(CancellationToken))
{
Check.IsDefined(entityState, "entityState");

var generatedValue = entityState == EntityState.Added && EntityState != EntityState.Added
? await GenerateKeyValue(cancellationToken).ConfigureAwait(false)
: null;
SetEntityState(entityState, await GenerateValuesAsync(GetValueGenerators(entityState), cancellationToken));
}

SetEntityState(entityState, generatedValue);
private Tuple<IProperty, object>[] GenerateValues(Tuple<IProperty, IValueGenerator>[] generators)
{
return generators == null
? null
: generators
.Select(t => t.Item2 == null ? null : Tuple.Create(t.Item1, t.Item2.Next(_configuration, t.Item1)))
.ToArray();
}

private Task<object> GenerateKeyValue(CancellationToken cancellationToken = default(CancellationToken))
private async Task<Tuple<IProperty, object>[]> GenerateValuesAsync(
Tuple<IProperty, IValueGenerator>[] generators,
CancellationToken cancellationToken = default(CancellationToken))
{
var keyProperty = _entityType.GetKey().Properties.Single(); // TODO: Composite keys not implemented yet.
var valueGenerator = _configuration.ValueGeneratorCache.GetGenerator(keyProperty);
if (generators == null)
{
return null;
}

var values = new Tuple<IProperty, object>[generators.Length];
for (var i = 0; i < generators.Length; i++)
{
var generator = generators[i].Item2;
if (generator != null)
{
var property = generators[i].Item1;
values[i] = Tuple.Create(property, await generator.NextAsync(_configuration, property, cancellationToken));
}
}
return values;
}

return valueGenerator != null
? valueGenerator.NextAsync(_configuration, keyProperty, cancellationToken)
: Task.FromResult<object>(null);
private Tuple<IProperty, IValueGenerator>[] GetValueGenerators(EntityState newState)
{
if (newState != EntityState.Added
|| EntityState == EntityState.Added)
{
return null;
}
var properties = _entityType.Properties.Where(p => p.ValueGenerationOnAdd != ValueGenerationOnAdd.None);
return properties.Select(p => Tuple.Create(p, _configuration.ValueGeneratorCache.GetGenerator(p))).ToArray();
}

private void SetEntityState(EntityState entityState, object generatedValue)
private void SetEntityState(EntityState newState, Tuple<IProperty, object>[] generatedValues)
{
// The entity state can be Modified even if some properties are not modified so always
// set all properties to modified if the entity state is explicitly set to Modified.
if (entityState == EntityState.Modified)
if (newState == EntityState.Modified)
{
_stateData.SetAllPropertiesModified(_entityType.Properties.Count());
}

var oldState = _stateData.EntityState;
if (oldState == entityState)
if (oldState == newState)
{
return;
}

_configuration.Services.StateEntryNotifier.StateChanging(this, entityState);
_configuration.Services.StateEntryNotifier.StateChanging(this, newState);

_stateData.EntityState = entityState;
_stateData.EntityState = newState;

if (entityState == EntityState.Added
&& generatedValue != null)
if (newState == EntityState.Added)
{
foreach (var generatedValue in generatedValues.Where(v => v != null))
{
this[generatedValue.Item1] = generatedValue.Item2;
}
}
else
{
this[_entityType.GetKey().Properties.Single()] = generatedValue; // TODO: Composite keys not implemented yet.
Contract.Assert(generatedValues == null);
}

if (oldState == EntityState.Unknown)
{
_configuration.Services.StateManager.StartTracking(this);
}
else if (entityState == EntityState.Unknown)
else if (newState == EntityState.Unknown)
{
// TODO: Does changing to Unknown really mean stop tracking?
_configuration.Services.StateManager.StopTracking(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.Entity.Metadata;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;

namespace Microsoft.Data.Entity.InMemory.FunctionalTests
{
public class CompositeKeyEndToEndTest
{
[Fact]
public async Task Can_use_two_non_generated_integers_as_composite_key_end_to_end()
{
var serviceProvider = new ServiceCollection()
.AddEntityFramework()
.AddInMemoryStore()
.ServiceCollection
.BuildServiceProvider();

var ticks = DateTime.UtcNow.Ticks;

using (var context = new BronieContext(serviceProvider))
{
await context.AddAsync(new Pegasus { Id1 = ticks, Id2 = ticks + 1, Name = "Rainbow Dash" });
await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
var pegasus = context.Pegasuses.Single(e => e.Id1 == ticks && e.Id2 == ticks + 1);

pegasus.Name = "Rainbow Crash";

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
var pegasus = context.Pegasuses.Single(e => e.Id1 == ticks && e.Id2 == ticks + 1);

Assert.Equal("Rainbow Crash", pegasus.Name);

context.Pegasuses.Remove(pegasus);

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
Assert.Equal(0, context.Pegasuses.Count(e => e.Id1 == ticks && e.Id2 == ticks + 1));
}
}

[Fact]
public async Task Can_use_generated_values_in_composite_key_end_to_end()
{
var serviceProvider = new ServiceCollection()
.AddEntityFramework()
.AddInMemoryStore()
.ServiceCollection
.BuildServiceProvider();

long id1;
var id2 = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
Guid id3;

using (var context = new BronieContext(serviceProvider))
{
var added = await context.AddAsync(new Unicorn { Id2 = id2, Name = "Rarity" });

Assert.True(added.Id1 > 0);
Assert.NotEqual(Guid.Empty, added.Id3);

await context.SaveChangesAsync();

id1 = added.Id1;
id3 = added.Id3;
}

using (var context = new BronieContext(serviceProvider))
{
Assert.Equal(1, context.Unicorns.Count(e => e.Id1 == id1 && e.Id2 == id2 && e.Id3 == id3));
}

using (var context = new BronieContext(serviceProvider))
{
var unicorn = context.Unicorns.Single(e => e.Id1 == id1 && e.Id2 == id2 && e.Id3 == id3);

unicorn.Name = "Bad Hair Day";

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
var unicorn = context.Unicorns.Single(e => e.Id1 == id1 && e.Id2 == id2 && e.Id3 == id3);

Assert.Equal("Bad Hair Day", unicorn.Name);

context.Unicorns.Remove(unicorn);

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
Assert.Equal(0, context.Unicorns.Count(e => e.Id1 == id1 && e.Id2 == id2 && e.Id3 == id3));
}
}

[Fact]
public async Task Only_one_part_of_a_composite_key_needs_to_vary_for_uniquness()
{
var serviceProvider = new ServiceCollection()
.AddEntityFramework()
.AddInMemoryStore()
.ServiceCollection
.BuildServiceProvider();

var ids = new int[3];

using (var context = new BronieContext(serviceProvider))
{
var pony1 = await context.AddAsync(new EarthPony { Id2 = 7, Name = "Apple Jack 1" });
var pony2 = await context.AddAsync(new EarthPony { Id2 = 7, Name = "Apple Jack 2" });
var pony3 = await context.AddAsync(new EarthPony { Id2 = 7, Name = "Apple Jack 3" });

await context.SaveChangesAsync();

ids[0] = pony1.Id1;
ids[1] = pony2.Id1;
ids[2] = pony3.Id1;
}

using (var context = new BronieContext(serviceProvider))
{
var ponies = context.EarthPonies.ToList();
Assert.Equal(ponies.Count, ponies.Count(e => e.Name == "Apple Jack 1") * 3);

Assert.Equal("Apple Jack 1", ponies.Single(e => e.Id1 == ids[0]).Name);
Assert.Equal("Apple Jack 2", ponies.Single(e => e.Id1 == ids[1]).Name);
Assert.Equal("Apple Jack 3", ponies.Single(e => e.Id1 == ids[2]).Name);

ponies.Single(e => e.Id1 == ids[1]).Name = "Pinky Pie 2";

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
var ponies = context.EarthPonies.ToList();
Assert.Equal(ponies.Count, ponies.Count(e => e.Name == "Apple Jack 1") * 3);

Assert.Equal("Apple Jack 1", ponies.Single(e => e.Id1 == ids[0]).Name);
Assert.Equal("Pinky Pie 2", ponies.Single(e => e.Id1 == ids[1]).Name);
Assert.Equal("Apple Jack 3", ponies.Single(e => e.Id1 == ids[2]).Name);

context.EarthPonies.RemoveRange(ponies);

await context.SaveChangesAsync();
}

using (var context = new BronieContext(serviceProvider))
{
Assert.Equal(0, context.EarthPonies.Count());
}
}

private class BronieContext : DbContext
{

public BronieContext(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}

public DbSet<Pegasus> Pegasuses { get; set; }
public DbSet<Unicorn> Unicorns { get; set; }
public DbSet<EarthPony> EarthPonies { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Pegasus>().Key(e => new { e.Id1, e.Id2 });
builder.Entity<Unicorn>().Key(e => new { e.Id1, e.Id2, e.Id3 });
builder.Entity<EarthPony>().Key(e => new { e.Id1, e.Id2 });

var unicornType = builder.Model.GetEntityType(typeof(Unicorn));

var id1 = unicornType.GetProperty("Id1");
id1.ValueGenerationOnAdd = ValueGenerationOnAdd.Client;
id1.ValueGenerationOnSave = ValueGenerationOnSave.WhenInserting;

var id3 = unicornType.GetProperty("Id3");
id3.ValueGenerationOnAdd = ValueGenerationOnAdd.Client;

var earthType = builder.Model.GetEntityType(typeof(EarthPony));

var id = earthType.GetProperty("Id1");
id.ValueGenerationOnAdd = ValueGenerationOnAdd.Client;
id.ValueGenerationOnSave = ValueGenerationOnSave.WhenInserting;
}
}

private class Pegasus
{
public long Id1 { get; set; }
public long Id2 { get; set; }
public string Name { get; set; }
}

private class Unicorn
{
public int Id1 { get; set; }
public string Id2 { get; set; }
public Guid Id3 { get; set; }
public string Name { get; set; }
}

private class EarthPony
{
public int Id1 { get; set; }
public int Id2 { get; set; }
public string Name { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="CompositeKeyEndToEndTest.cs" />
<Compile Include="GuidValueGeneratorEndToEndTest.cs" />
<Compile Include="InMemoryConfigPatternsTest.cs" />
<Compile Include="InMemoryDataStoreTest.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public NorthwindQueryFixture()
context.Set<Employee>().AddRange(NorthwindData.Employees);
context.Set<Order>().AddRange(NorthwindData.Orders);
context.Set<Product>().AddRange(NorthwindData.Products);
//context.Set<OrderDetail>().AddRange(NorthwindData.OrderDetails); // composite keys
context.Set<OrderDetail>().AddRange(NorthwindData.OrderDetails);
context.SaveChanges();
}
}
Expand Down
Loading

0 comments on commit 1a376a0

Please sign in to comment.