Skip to content

Commit

Permalink
Merge pull request #151 from MicrosoftEdge/release/8.1.0
Browse files Browse the repository at this point in the history
Release/8.1.0
  • Loading branch information
rjmurillo committed Jul 27, 2017
2 parents d5e1365 + dc4bc9d commit 3efa72e
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 30 deletions.
46 changes: 41 additions & 5 deletions src/Qwiq.Core.Rest/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ private WorkItem CreateItemEager(TeamFoundation.WorkItemTracking.WebApi.Models.W
{
return new WorkItem(
workItem,
// REVIEW: Allocate for reference type
_workItemStore.Projects[(string)workItem.Fields[CoreFieldRefNames.TeamProject]]
.WorkItemTypes[(string)workItem.Fields[CoreFieldRefNames.WorkItemType]],
LookUpWorkItemType(workItem),
// REVIEW: Delegate allocation from method group
LinkFunc);
}
Expand All @@ -103,8 +101,7 @@ private WorkItem CreateItemLazy(TeamFoundation.WorkItemTracking.WebApi.Models.Wo
{
IWorkItemType WorkItemTypeFactory()
{
return _workItemStore.Projects[(string)workItem.Fields[CoreFieldRefNames.TeamProject]]
.WorkItemTypes[(string)workItem.Fields[CoreFieldRefNames.WorkItemType]];
return LookUpWorkItemType(workItem);
}

return new WorkItem(workItem, new Lazy<IWorkItemType>(WorkItemTypeFactory), LinkFunc);
Expand Down Expand Up @@ -160,6 +157,45 @@ private List<IWorkItem> LoadWorkItemsEagerly(List<TeamFoundation.WorkItemTrackin
return retval;
}

[JetBrains.Annotations.Pure]
[NotNull]
private IWorkItemType LookUpWorkItemType([NotNull] TeamFoundation.WorkItemTracking.WebApi.Models.WorkItem workItem)
{
if (!workItem.Fields.TryGetValue(CoreFieldRefNames.TeamProject, out object tp))
{
throw new InvalidOperationException($"Field '{CoreFieldRefNames.TeamProject}' is required.");
}
if (!workItem.Fields.TryGetValue(CoreFieldRefNames.WorkItemType, out object wit))
{
throw new InvalidOperationException($"Field '{CoreFieldRefNames.WorkItemType}' is required.");
}

var tps = tp as string;
var wits = wit as string;

if (string.IsNullOrWhiteSpace(tps))
{
throw new InvalidOperationException(
$"Value for field '{CoreFieldRefNames.TeamProject}' cannot be null or empty.");
}
if (string.IsNullOrWhiteSpace(wits))
{
throw new InvalidOperationException(
$"Value for field '{CoreFieldRefNames.WorkItemType}' cannot be null or empty.");
}

if (!_workItemStore.Projects.Contains(tps))
{
throw new InvalidOperationException($"No project for specified value '{tps}'.");
}
var proj = _workItemStore.Projects[tps];
if (!proj.WorkItemTypes.Contains(wits))
{
throw new InvalidOperationException($"No work item type for specified value '{wits}'.");
}
return proj.WorkItemTypes[wits];
}

private ReadOnlyCollection<IWorkItemLinkInfo> RunkLinkQueryImpl()
{
// Eager loading for the link type ID (which is not returned by the REST API) causes ~250ms delay
Expand Down
7 changes: 7 additions & 0 deletions src/Qwiq.Core.Rest/WorkItem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;

Expand Down Expand Up @@ -143,6 +144,9 @@ protected override object GetValue(string name)
if (string.IsNullOrEmpty(name)) return null;

_item.Fields.TryGetValue(name, out object value);
#if DEBUG
Trace.WriteLine($"Get \'{name}\': {value.ToUsefulString()}");
#endif
return value;
}

Expand All @@ -151,6 +155,9 @@ protected override void SetValue(string name, object value)
if (string.IsNullOrEmpty(name)) return;

_item.Fields[name] = value;
#if DEBUG
Trace.WriteLine($"Set \'{name}\' to {value.ToUsefulString()}");
#endif
}

[ContractInvariantMethod]
Expand Down
24 changes: 14 additions & 10 deletions src/Qwiq.Core/WorkItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ public abstract class WorkItem : WorkItemCommon, IWorkItem, IRevisionInternal, I

private bool _useFields = true;

protected internal WorkItem([NotNull] IWorkItemType type, [CanBeNull] Dictionary<string, object> fields)
protected internal WorkItem([NotNull] IWorkItemType workItemType, [CanBeNull] Dictionary<string, object> fields)
: base(fields)
{
Contract.Requires(type != null);
Contract.Requires(workItemType != null);

_type = type ?? throw new ArgumentNullException(nameof(type));
_type = workItemType ?? throw new ArgumentNullException(nameof(workItemType));
}

protected internal WorkItem([NotNull] IWorkItemType type)
protected internal WorkItem([NotNull] IWorkItemType workItemType)
{
Contract.Requires(type != null);
Contract.Requires(workItemType != null);

_type = type ?? throw new ArgumentNullException(nameof(type));
_type = workItemType ?? throw new ArgumentNullException(nameof(workItemType));
}

protected internal WorkItem([NotNull] Lazy<IWorkItemType> type)
Expand All @@ -48,11 +48,11 @@ protected internal WorkItem([NotNull] Lazy<IWorkItemType> type)
_lazyType = type;
}

protected internal WorkItem([NotNull] IWorkItemType type, [NotNull] Func<IFieldCollection> fieldCollectionFactory)
protected internal WorkItem([NotNull] IWorkItemType workItemType, [NotNull] Func<IFieldCollection> fieldCollectionFactory)
{
Contract.Requires(type != null);
Contract.Requires(workItemType != null);
Contract.Requires(fieldCollectionFactory != null);
_type = type ?? throw new ArgumentNullException(nameof(type));
_type = workItemType ?? throw new ArgumentNullException(nameof(workItemType));
_fieldFactory = fieldCollectionFactory ?? throw new ArgumentNullException(nameof(fieldCollectionFactory));
}

Expand Down Expand Up @@ -110,7 +110,7 @@ public virtual string Keywords

public virtual IEnumerable<IRevision> Revisions => throw new NotSupportedException();

public virtual IWorkItemType Type => _type ?? _lazyType?.Value ?? throw new NotSupportedException();
public virtual IWorkItemType Type => _type ?? _lazyType?.Value ?? throw new InvalidOperationException($"No value specified for {nameof(Type)}.");

public abstract Uri Uri { get; }

Expand All @@ -124,6 +124,10 @@ public virtual string Keywords
{
return Fields[name].Value;
}
catch (InvalidOperationException ioex) when (ioex.Source == "Microsoft.Qwiq.Client.Rest")
{
_useFields = false;
}
catch (NotSupportedException)
{
_useFields = false;
Expand Down
8 changes: 4 additions & 4 deletions src/Qwiq.Mapper/Attributes/AttributeMapperStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ private static IEnumerable<PropertyInfo> PropertiesOnWorkItemCache(IPropertyInsp
{
// Composite key: work item type and target type

var workItemType = workItem.Type.Name;
var workItemType = workItem.WorkItemType;
var key = new Tuple<string, RuntimeTypeHandle>(workItemType, targetType.TypeHandle);

return PropertiesThatExistOnWorkItem.GetOrAdd(
Expand All @@ -167,9 +167,9 @@ private static IEnumerable<PropertyInfo> PropertiesOnWorkItemCache(IPropertyInsp
property =>
new { property, fieldName = PropertyInfoFieldCache(inspector, property)?.FieldName })
.Where(
@t =>
!string.IsNullOrEmpty(@t.fieldName) && workItem.Fields.Contains(@t.fieldName))
.Select(@t => @t.property)
t =>
!string.IsNullOrEmpty(t.fieldName))
.Select(t => t.property)
.ToList();
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/Qwiq.Mapper/Attributes/WorkItemLinksMapperStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public WorkItemLinksMapperStrategy(IPropertyInspector inspector, IWorkItemStore
{
// Composite key: work item type and target type

var workItemType = workItem.Type.Name;
var workItemType = workItem.WorkItemType;
var key = new Tuple<string, RuntimeTypeHandle>(workItemType, targetType.TypeHandle);

return PropertiesThatExistOnWorkItem.GetOrAdd(
Expand Down
98 changes: 98 additions & 0 deletions test/Qwiq.Integration.Tests/Mapper/AttributeMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Should;
using System;
using System.Linq;

namespace Microsoft.Qwiq.Mapper
{
[TestClass]
public class
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_TeamProject_and_WorkItemType_specified :
WiqlAttributeMapperContextSpecification
{
[TestMethod]
[TestCategory("localOnly")]
[TestCategory("REST")]
public void The_work_items_are_mapped_to_their_model()
{
Bugs.ToList().Count.ShouldEqual(1);
}

protected override void ConfigureOptions()
{
WorkItemStore.Configuration.DefaultFields = new[]
{
//CoreFieldRefNames.TeamProject,
//CoreFieldRefNames.WorkItemType,
CoreFieldRefNames.Id,
CoreFieldRefNames.State
};
}
}

[TestClass]
public class
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_TeamProject_and_WorkItemType_specified_Eager :
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_TeamProject_and_WorkItemType_specified
{
[TestMethod]
[TestCategory("localOnly")]
[TestCategory("REST")]
[ExpectedException(typeof(InvalidOperationException), "The fields '" + CoreFieldRefNames.TeamProject + "' and '" + CoreFieldRefNames.WorkItemType + "' are required to load the Type property.")]
public new void The_work_items_are_mapped_to_their_model()
{
Bugs.ToList().Count.ShouldEqual(1);
}

protected override void ConfigureOptions()
{
base.ConfigureOptions();
WorkItemStore.Configuration.LazyLoadingEnabled = false;
}
}

[TestClass]
public class
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_WorkItemType_specified :
WiqlAttributeMapperContextSpecification
{
[TestMethod]
[TestCategory("localOnly")]
[TestCategory("REST")]
public void The_work_items_are_mapped_to_their_model()
{
Bugs.ToList().Count.ShouldEqual(1);
}

protected override void ConfigureOptions()
{
WorkItemStore.Configuration.DefaultFields = new[]
{
CoreFieldRefNames.TeamProject,
//CoreFieldRefNames.WorkItemType,
CoreFieldRefNames.Id,
CoreFieldRefNames.State
};
}
}
[TestClass]
public class
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_WorkItemType_specified_Eager :
Given_an_AttributeMapper_with_a_WorkItemStore_DefaultFields_without_WorkItemType_specified
{
[TestMethod]
[TestCategory("localOnly")]
[TestCategory("REST")]
[ExpectedException(typeof(InvalidOperationException), "The fields '" + CoreFieldRefNames.TeamProject + "' and '" + CoreFieldRefNames.WorkItemType + "' are required to load the Type property.")]
public new void The_work_items_are_mapped_to_their_model()
{
Bugs.ToList().Count.ShouldEqual(1);
}

protected override void ConfigureOptions()
{
base.ConfigureOptions();
WorkItemStore.Configuration.LazyLoadingEnabled = false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Microsoft.Qwiq.Linq;
using Microsoft.Qwiq.Linq.Visitors;
using Microsoft.Qwiq.Mapper.Attributes;
using Microsoft.Qwiq.Tests.Common;
using System.Linq;

namespace Microsoft.Qwiq.Mapper
{
public abstract class WiqlAttributeMapperContextSpecification : TimedContextSpecification
{
private int[] _ids;
public IQueryable<Bug> Bugs { get; set; }
protected IWorkItemStore WorkItemStore { get; private set; }
private Query<Bug> Query { get; set; }
public override void Cleanup()
{
WorkItemStore?.Dispose();
base.Cleanup();
}

public override void Given()
{
base.Given();

WorkItemStore = TimedAction(() => IntegrationSettings.CreateRestStore(), "REST", "WIS Create");

ConfigureOptions();

var pr = new PropertyReflector();
var pi = new PropertyInspector(pr);
var attMapper = new AttributeMapperStrategy(pi);
var mapper = new WorkItemMapper(new IWorkItemMapperStrategy[] { attMapper });
var translator = new WiqlTranslator();
var pe = new PartialEvaluator();
var qr = new QueryRewriter();
var wqb = new WiqlQueryBuilder(translator, pe, qr);
var qp = new MapperTeamFoundationServerWorkItemQueryProvider(
WorkItemStore,
wqb,
mapper);

Query = new Query<Bug>(qp, wqb);

_ids = new[]
{
8663955
};
}

public override void When()
{
Bugs = Query.Where(b => _ids.Contains(b.Id.Value));
}

protected abstract void ConfigureOptions();
[WorkItemType("Bug")]
public class Bug : IIdentifiable<int?>
{
[FieldDefinition(CoreFieldRefNames.Id, true)]
public int? Id { get; set; }

[FieldDefinition(CoreFieldRefNames.State)]
public string State { get; set; }

[FieldDefinition("InvalidField")]
public string Invalid { get; set; }
}
}
}
6 changes: 6 additions & 0 deletions test/Qwiq.Integration.Tests/Qwiq.IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@
<Compile Include="Identity\Soap\SoapIdentityMapperContextSpecification.cs" />
<Compile Include="IntegrationSettings.cs" />
<Compile Include="Linq\LinqContextSpecification.cs" />
<Compile Include="Mapper\AttributeMapperTests.cs" />
<Compile Include="Mapper\WiqlAttributeMapperContextSpecification.cs" />
<Compile Include="Mapper\Identity\DisplayNameToAliasConverterContextSpecification.cs" />
<Compile Include="Mapper\Identity\DisplayNameToAliasConverterTests.cs" />
<Compile Include="Mapper\Identity\MultipleDisplayNameContextSpecification.cs" />
Expand Down Expand Up @@ -264,6 +266,10 @@
<Project>{BE25CF2D-FA53-4455-85B1-4EEC1D979FB1}</Project>
<Name>Qwiq.Mapper.Identity</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Qwiq.Mapper\Qwiq.Mapper.csproj">
<Project>{016E8D93-4195-4639-BCD5-77633E8E1681}</Project>
<Name>Qwiq.Mapper</Name>
</ProjectReference>
<ProjectReference Include="..\Qwiq.Tests.Common\Qwiq.Tests.Common.csproj">
<Project>{B45C92B0-AC36-409D-86A5-5428C87384C3}</Project>
<Name>Qwiq.Tests.Common</Name>
Expand Down
Loading

0 comments on commit 3efa72e

Please sign in to comment.