Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ creates each record and updates it if it already exists.
- Multi-select choice columns (`OptionSetValueCollection`) export and import correctly
- Floating point (`Double`) columns can be imported from every format, not only Full
- Records with date columns are skipped as `(Identical)` when nothing changed. A date read from a file and the same date read from the target used to compare as different, so these records were always updated, and a `Match` on a date column created a duplicate
- An N:N relationship from a table to itself (intersect columns `<table>idone` and `<table>idtwo`) imports. Every record used to fail, because the table name was taken as `<table>idone`

### Solutions
- A zip without `solution.xml` fails with a clear `FileNotFoundException`
Expand Down
1 change: 1 addition & 0 deletions XTB/ShuffleDeployer.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Import
- Multi-select choice columns (OptionSetValueCollection) export and import correctly.
- Floating point (Double) columns can be imported from every format, not only Full.
- Records with date columns are now skipped as identical when nothing changed, and a Match on a date column finds the existing record instead of creating a duplicate.
- An N:N relationship from a table to itself imports; every record used to fail.

Logs
- Values instead of SDK type names, no more "[START of ToString]" noise, and correctly nested sections, also after an error.
Expand Down
1 change: 1 addition & 0 deletions XTB/ShuffleRunner.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Import
- Multi-select choice columns (OptionSetValueCollection) export and import correctly.
- Floating point (Double) columns can be imported from every format, not only Full.
- Records with date columns are now skipped as identical when nothing changed, and a Match on a date column finds the existing record instead of creating a duplicate.
- An N:N relationship from a table to itself imports; every record used to fail.

Runner
- The import no longer waits for the window to repaint after every record, which made large blocks several times slower.
Expand Down
19 changes: 17 additions & 2 deletions shared/Xrm.Shuffle.Core/ShuffleDataImport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -786,12 +786,27 @@ private EntityReference GetAttributeEntityReference(KeyValuePair<string, object>
{
id = guidmap[id];
}
var logicname = x.Key.EndsWith("id") ? x.Key.Substring(0, x.Key.Length - 2) : x.Key;
return new EntityReference(logicname, id);
return new EntityReference(TableOfIntersectColumn(x.Key), id);
}
return null;
}

/// <summary>
/// The table an intersect column points at: &lt;table&gt;id, or &lt;table&gt;idone and
/// &lt;table&gt;idtwo for an N:N relationship from a table to itself.
/// </summary>
private static string TableOfIntersectColumn(string column)
{
foreach (var suffix in new[] { "idone", "idtwo", "id" })
{
if (column.Length > suffix.Length && column.EndsWith(suffix, StringComparison.Ordinal))
{
return column.Substring(0, column.Length - suffix.Length);
}
}
return column;
}

/// <summary>
/// Determines whether the record holds a reference to a record that is still waiting in the
/// create batch. Such a reference cannot be remapped by <see cref="ReplaceGuids"/> yet, because
Expand Down
96 changes: 96 additions & 0 deletions tests/Xrm.Shuffle.Core.Tests/Layer2/IntersectImportTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace Cinteros.Crm.Utils.Shuffle.Tests.Layer2
{
using System;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using Cinteros.Crm.Utils.Shuffle.Tests.Helpers;
using FakeXrmEasy;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using NUnit.Framework;

/// <summary>
/// An intersect block associates the two records each row names. The table on each side is
/// worked out from the intersect column - and for a relationship from a table to itself those
/// columns are &lt;table&gt;idone and &lt;table&gt;idtwo, not &lt;table&gt;id.
/// </summary>
[TestFixture]
public class IntersectImportTests : FakeOrgTestBase
{
private static Types.DataBlock IntersectBlock(string intersect, string column1, string column2)
{
var xml =
"<ShuffleDefinition><Blocks>" +
"<DataBlock Name=\"Links\" Entity=\"" + intersect + "\" Type=\"Intersect\">" +
"<Export><Attributes><Attribute Name=\"" + column1 + "\" /><Attribute Name=\"" + column2 + "\" /></Attributes></Export>" +
"<Import />" +
"</DataBlock></Blocks></ShuffleDefinition>";
using (var reader = new StringReader(xml))
{
var definition = (Types.ShuffleDefinition)new XmlSerializer(typeof(Types.ShuffleDefinition)).Deserialize(reader);
return (Types.DataBlock)definition.Blocks.Items[0];
}
}

/// <summary>One intersect row as the data file holds it: the two ids as bare Guids.</summary>
private static EntityCollection Rows(string intersect, string column1, string column2, params Tuple<Guid, Guid>[] pairs)
{
var rows = new EntityCollection { EntityName = intersect };
foreach (var pair in pairs)
{
var row = new Entity(intersect);
row[column1] = pair.Item1;
row[column2] = pair.Item2;
rows.Entities.Add(row);
}
return rows;
}

private AssociateRequest[] Associations() =>
Service.Requests.OfType<AssociateRequest>().ToArray();

[Test]
public void A_relationship_from_a_table_to_itself_associates_the_two_records()
{
Online().WithEntity(Record("new_category", Id(1), "Gold"), Record("new_category", Id(2), "Silver"), Record("new_category", Id(3), "Bronze"));
Org.Faked.AddRelationship("new_category_incompatible", new XrmFakedRelationship(
"new_category_incompatible", "new_categoryidone", "new_categoryidtwo", "new_category", "new_category"));

var outcome = NewShuffler().TestImportDataBlock(
IntersectBlock("new_category_incompatible", "new_categoryidone", "new_categoryidtwo"),
Rows("new_category_incompatible", "new_categoryidone", "new_categoryidtwo",
Tuple.Create(Id(1), Id(2)), Tuple.Create(Id(1), Id(3))));

Assert.That(outcome.Failed, Is.EqualTo(0), DumpAll());
Assert.That(outcome.Created, Is.EqualTo(2), DumpAll());
var associations = Associations();
Assert.That(associations.Select(a => a.Target.LogicalName), Is.All.EqualTo("new_category"), DumpAll());
Assert.That(associations.SelectMany(a => a.RelatedEntities).Select(r => r.LogicalName), Is.All.EqualTo("new_category"), DumpAll());
Assert.That(associations.Select(a => a.Relationship.PrimaryEntityRole), Is.All.EqualTo(Microsoft.Xrm.Sdk.EntityRole.Referencing),
"Dataverse rejects an associate on a reflexive relationship that does not say which side the target is on");
Assert.That(associations.Select(a => a.Target.Id), Is.EqualTo(new[] { Id(1), Id(1) }));
Assert.That(associations.Select(a => a.RelatedEntities.Single().Id), Is.EqualTo(new[] { Id(2), Id(3) }));
}

/// <summary>The usual case, two different tables with &lt;table&gt;id columns, is unchanged.</summary>
[Test]
public void A_relationship_between_two_tables_associates_them_without_a_role()
{
Online().WithEntity(Record("account", Id(1), "Contoso"), Record("contact", Id(2), "Pat"));
Org.Faked.AddRelationship("new_account_contact", new XrmFakedRelationship(
"new_account_contact", "accountid", "contactid", "account", "contact"));

var outcome = NewShuffler().TestImportDataBlock(
IntersectBlock("new_account_contact", "accountid", "contactid"),
Rows("new_account_contact", "accountid", "contactid", Tuple.Create(Id(1), Id(2))));

Assert.That(outcome.Failed, Is.EqualTo(0), DumpAll());
Assert.That(outcome.Created, Is.EqualTo(1), DumpAll());
var association = Associations().Single();
Assert.That(association.Target.LogicalName, Is.EqualTo("account"));
Assert.That(association.RelatedEntities.Single().LogicalName, Is.EqualTo("contact"));
Assert.That(association.Relationship.PrimaryEntityRole, Is.Null);
}
}
}
1 change: 1 addition & 0 deletions tests/Xrm.Shuffle.Core.Tests/Xrm.Shuffle.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<Compile Include="Layer2\BatchabilityTests.cs" />
<Compile Include="Layer2\CapabilityDetectionTests.cs" />
<Compile Include="Layer2\ExportAttributeSelectionTests.cs" />
<Compile Include="Layer2\IntersectImportTests.cs" />
<Compile Include="Layer2\MatchResolutionTests.cs" />
<Compile Include="Layer2\PreRetrievedMatchingTests.cs" />
<Compile Include="Layer2\SkipIdenticalTests.cs" />
Expand Down
Loading